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.
| Package | Affected versions | Patched versions |
|---|---|---|
tarnpm | < 7.5.11 | 7.5.11 |
Affected products
1Patches
1f48b5fa3b798prevent escaping symlinks with drive-relative paths
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- github.com/advisories/GHSA-9ppj-qmqm-q256ghsaADVISORY
- nvd.nist.gov/vuln/detail/CVE-2026-31802ghsaADVISORY
- github.com/isaacs/node-tar/commit/f48b5fa3b7985ddab96dc0f2125a4ffc9911b6adghsax_refsource_MISCWEB
- github.com/isaacs/node-tar/security/advisories/GHSA-9ppj-qmqm-q256ghsax_refsource_CONFIRMWEB
News mentions
0No linked articles in our index yet.