VYPR
High severityNVD Advisory· Published Mar 9, 2026· Updated Mar 10, 2026

node-tar Symlink Path Traversal via Drive-Relative Linkpath

CVE-2026-31802

Description

node-tar is a full-featured Tar for Node.js. Prior to version 7.5.11, 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. This vulnerability is fixed in 7.5.11.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
tarnpm
< 7.5.117.5.11

Affected products

1

Patches

1
f48b5fa3b798

prevent escaping symlinks with drive-relative paths

https://github.com/isaacs/node-tarisaacsMar 9, 2026via ghsa
3 files changed · +76 3
  • src/unpack.ts+1 1 modified
    @@ -303,7 +303,7 @@ export class Unpack extends Parser {
             // tar paths, not a filesystem.
             const entryDir = path.posix.dirname(entry.path)
             const resolved = path.posix.normalize(
    -          path.posix.join(entryDir, p),
    +          path.posix.join(entryDir, parts.join('/'))
             )
             // If the resolved path escapes (starts with ..), reject it
             if (resolved.startsWith('../') || resolved === '..') {
    
  • tap-snapshots/test/ghsa-8qq5-rm4j-mr97.ts.test.cjs+43 0 added
    @@ -0,0 +1,43 @@
    +/* IMPORTANT
    + * This snapshot file is auto-generated, but designed for humans.
    + * It should be checked into source control and tracked carefully.
    + * Re-generate by setting TAP_SNAPSHOT=1 and running tests.
    + * Make sure to inspect the output below.  Do not ignore changes!
    + */
    +'use strict'
    +exports[`test/ghsa-8qq5-rm4j-mr97.ts > TAP > warnings > must match snapshot 1`] = `
    +Array [
    +  Array [
    +    "TAR_ENTRY_INFO",
    +    "stripping / from absolute linkpath",
    +  ],
    +  Array [
    +    "TAR_ENTRY_ERROR",
    +    "ENOENT: no such file or directory, link",
    +  ],
    +  Array [
    +    "TAR_ENTRY_ERROR",
    +    "linkpath contains '..'",
    +  ],
    +  Array [
    +    "TAR_ENTRY_INFO",
    +    "stripping / from absolute linkpath",
    +  ],
    +  Array [
    +    "TAR_ENTRY_ERROR",
    +    "linkpath escapes extraction directory",
    +  ],
    +  Array [
    +    "TAR_ENTRY_INFO",
    +    "stripping / from absolute linkpath",
    +  ],
    +  Array [
    +    "TAR_ENTRY_INFO",
    +    "stripping c: from absolute linkpath",
    +  ],
    +  Array [
    +    "TAR_ENTRY_ERROR",
    +    "linkpath escapes extraction directory",
    +  ],
    +]
    +`
    
  • test/ghsa-8qq5-rm4j-mr97.ts+32 2 modified
    @@ -11,6 +11,14 @@ import { Header, x } from 'tar'
     const targetSym = '/some/absolute/path'
     const absoluteWithDotDot = '/../a/target'
     
    +const secretLinkpath = resolve(t.testdirName, 'secret.txt')
    +
    +t.formatSnapshot = (o: unknown): unknown =>
    +  typeof o === 'string' ? o.replace(
    +    /^ENOENT: no such file or directory, link .*? -> .*?$/, 'ENOENT: no such file or directory, link')
    +  : Array.isArray(o) ? o.map(o => t.formatSnapshot?.(o))
    +  : o
    +
     const getExploitTar = () => {
       const chunks: Buffer[] = []
     
    @@ -19,7 +27,7 @@ const getExploitTar = () => {
         path: 'exploit_hard',
         type: 'Link',
         size: 0,
    -    linkpath: resolve(t.testdirName, 'secret.txt'),
    +    linkpath: secretLinkpath,
       }).encode(hardHeader, 0)
       chunks.push(hardHeader)
     
    @@ -78,6 +86,14 @@ const getExploitTar = () => {
       }).encode(winAbsWithDotDotHeader, 0)
       chunks.push(winAbsWithDotDotHeader)
     
    +  const winAbsWithDotDotEscapeHeader = Buffer.alloc(512)
    +  new Header({
    +    path: 'a/winrootdotsescapelink',
    +    type: 'SymbolicLink',
    +    linkpath: 'c:..\\..\\..\\..\\foo\\bar',
    +  }).encode(winAbsWithDotDotEscapeHeader, 0)
    +  chunks.push(winAbsWithDotDotEscapeHeader)
    +
       chunks.push(Buffer.alloc(1024))
       return Buffer.concat(chunks)
     }
    @@ -91,7 +107,17 @@ const dir = t.testdir({
     const out = resolve(dir, 'out')
     const tarFile = resolve(dir, 'exploit.tar')
     
    -t.before(() => x({ cwd: out, file: tarFile }))
    +const WARNINGS: [code: string, message: string][] = []
    +t.before(() =>
    +  x({
    +    cwd: out,
    +    file: tarFile,
    +    onwarn: (code: string, message: string) =>
    +      WARNINGS.push([code, message]),
    +  }),
    +)
    +
    +t.test('warnings', async t => t.matchSnapshot(WARNINGS))
     
     t.test('writefile exploits fail', async t => {
       writeFileSync(resolve(out, 'exploit_hard'), 'OVERWRITTEN')
    @@ -126,4 +152,8 @@ t.test('absolute symlink with .. has prefix stripped', async t => {
         '..\\foo\\bar',
         'symlink target should be normalized',
       )
    +  t.throws(
    +    () => lstatSync(resolve(out, 'a/winrootdotsescapelink')),
    +    'escaping symlink is not created',
    +  )
     })
    

Vulnerability mechanics

Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

4

News mentions

0

No linked articles in our index yet.