VYPR
High severityOSV Advisory· Published Jan 16, 2026· Updated Jan 20, 2026

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

CVE-2026-23745

Description

node-tar is a Tar for Node.js. The node-tar library (<= 7.5.2) fails to sanitize the linkpath of Link (hardlink) and SymbolicLink entries when preservePaths is false (the default secure behavior). This allows malicious archives to bypass the extraction root restriction, leading to Arbitrary File Overwrite via hardlinks and Symlink Poisoning via absolute symlink targets. This vulnerability is fixed in 7.5.3.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
tarnpm
< 7.5.37.5.3

Affected products

1

Patches

1
340eb285b6d9

fix: sanitize absolute linkpaths properly

https://github.com/isaacs/node-tarisaacsJan 16, 2026via ghsa
2 files changed · +94 26
  • src/unpack.ts+46 26 modified
    @@ -33,6 +33,7 @@ const SYMLINK = Symbol('symlink')
     const HARDLINK = Symbol('hardlink')
     const UNSUPPORTED = Symbol('unsupported')
     const CHECKPATH = Symbol('checkPath')
    +const STRIPABSOLUTEPATH = Symbol('stripAbsolutePath')
     const MKDIR = Symbol('mkdir')
     const ONERROR = Symbol('onError')
     const PENDING = Symbol('pending')
    @@ -263,6 +264,46 @@ export class Unpack extends Parser {
         }
       }
     
    +  // return false if we need to skip this file
    +  // return true if the field was successfully sanitized
    +  [STRIPABSOLUTEPATH](
    +    entry: ReadEntry,
    +    field: 'path' | 'linkpath',
    +  ): boolean {
    +    const path = entry[field]
    +    if (!path || this.preservePaths) return true
    +
    +    const parts = path.split('/')
    +    if (
    +      parts.includes('..') ||
    +      /* c8 ignore next */
    +      (isWindows && /^[a-z]:\.\.$/i.test(parts[0] ?? ''))
    +    ) {
    +      this.warn('TAR_ENTRY_ERROR', `${field} contains '..'`, {
    +        entry,
    +        [field]: path,
    +      })
    +      // not ok!
    +      return false
    +    }
    +
    +    // strip off the root
    +    const [root, stripped] = stripAbsolutePath(path)
    +    if (root) {
    +      // ok, but triggers warning about stripping root
    +      entry[field] = String(stripped)
    +      this.warn(
    +        'TAR_ENTRY_INFO',
    +        `stripping ${root} from absolute ${field}`,
    +        {
    +          entry,
    +          [field]: path,
    +        },
    +      )
    +    }
    +    return true
    +  }
    +
       [CHECKPATH](entry: ReadEntry) {
         const p = normalizeWindowsPath(entry.path)
         const parts = p.split('/')
    @@ -295,32 +336,11 @@ export class Unpack extends Parser {
           return false
         }
     
    -    if (!this.preservePaths) {
    -      if (
    -        parts.includes('..') ||
    -        /* c8 ignore next */
    -        (isWindows && /^[a-z]:\.\.$/i.test(parts[0] ?? ''))
    -      ) {
    -        this.warn('TAR_ENTRY_ERROR', `path contains '..'`, {
    -          entry,
    -          path: p,
    -        })
    -        return false
    -      }
    -
    -      // strip off the root
    -      const [root, stripped] = stripAbsolutePath(p)
    -      if (root) {
    -        entry.path = String(stripped)
    -        this.warn(
    -          'TAR_ENTRY_INFO',
    -          `stripping ${root} from absolute path`,
    -          {
    -            entry,
    -            path: p,
    -          },
    -        )
    -      }
    +    if (
    +      !this[STRIPABSOLUTEPATH](entry, 'path') ||
    +      !this[STRIPABSOLUTEPATH](entry, 'linkpath')
    +    ) {
    +      return false
         }
     
         if (path.isAbsolute(entry.path)) {
    
  • test/ghsa-8qq5-rm4j-mr97.ts+48 0 added
    @@ -0,0 +1,48 @@
    +import { readFileSync, readlinkSync, writeFileSync } from 'fs'
    +import { resolve } from 'path'
    +import t from 'tap'
    +import { Header, x } from 'tar'
    +
    +const targetSym = '/some/absolute/path'
    +
    +const getExploitTar = () => {
    +  const exploitTar = Buffer.alloc(512 + 512 + 1024)
    +
    +  new Header({
    +    path: 'exploit_hard',
    +    type: 'Link',
    +    size: 0,
    +    linkpath: resolve(t.testdirName, 'secret.txt'),
    +  }).encode(exploitTar, 0)
    +
    +  new Header({
    +    path: 'exploit_sym',
    +    type: 'SymbolicLink',
    +    size: 0,
    +    linkpath: targetSym,
    +  }).encode(exploitTar, 512)
    +
    +  return exploitTar
    +}
    +
    +const dir = t.testdir({
    +  'secret.txt': 'ORIGINAL DATA',
    +  'exploit.tar': getExploitTar(),
    +  out_repro: {},
    +})
    +
    +const out = resolve(dir, 'out_repro')
    +const tarFile = resolve(dir, 'exploit.tar')
    +
    +t.test('verify that linkpaths get sanitized properly', async t => {
    +  await x({
    +    cwd: out,
    +    file: tarFile,
    +    preservePaths: false,
    +  })
    +
    +  writeFileSync(resolve(out, 'exploit_hard'), 'OVERWRITTEN')
    +  t.equal(readFileSync(resolve(dir, 'secret.txt'), 'utf8'), 'ORIGINAL DATA')
    +
    +  t.not(readlinkSync(resolve(out, 'exploit_sym')), targetSym)
    +})
    

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.