l3montree-dev / l3montree-dev/devguard-documentation
GHSA-r292-9mhp-454m found in npm/tar@7.5.19
Nobody has claimed this yet.
- Dominant language
- MDX
- Stars
- 7
- Forks
- 5
- Avg merge
- 1d 21h
- Merged PRs (30d)
- 13
Description
GHSA-r292-9mhp-454m found in npm/tar@7.5.19
[!important]
Risk:3.46 (Low)
CVSS:7.5
Description
Summary
node-tar (npm tar) contains an uncontrolled-recursion stack-exhaustion DoS in the internal mapHas helper used by filesFilter. When a consumer calls tar.t(...) or tar.x(...) with a non-empty member-selection list, node-tar installs a filter that closes over the recursive mapHas (src/list.ts:33-44). mapHas walks an entry path upward one path.dirname() call per recursion with no segment cap. A single crafted tar with a GNU-L (or PAX-x) long-path header can deliver a path of tens of thousands of /-separated segments (up to maxMetaEntrySize = 1 MiB). The recursion overflows the call stack, throwing an uncatchable RangeError that terminates the Node process on async/streaming consumers.
Root Cause
filesFilter (src/list.ts:27-51) is installed whenever a caller passes a member-selection list (src/list.ts:119-122, src/extract.ts:55-57). Its filter is invoked at src/parse.ts:253 (entry.ignore = entry.ignore || !this.filter(entry.path, entry)) inside Parser[CONSUMEHEADER] — and crucially outside the only try/catch in that method (which wraps new Header at src/parse.ts:179-183). mapHas recurses once per path segment with no depth limit. The Unpack maxDepth guard (src/unpack.ts:342, in [CHECKPATH]) only runs on the 'entry' event, which fires after CONSUMEHEADER has already invoked the filter — so the stack overflows before any depth guard executes. tar.t (list) has no maxDepth at all.
Impact
Unauthenticated, remotely-triggerable denial of service: a ~188-byte gzip (≈26 KB tar) crashes any service that lists or extracts selected members from an untrusted archive (package registries, CI artifact/cache restore, upload processors). On async (await tar.t(...)/tar.x(...)) and streaming/pipe consumers the RangeError escapes the promise as an uncaughtException and terminates the process — standard defensive try/catch around the async call does NOT prevent it. (The synchronous API is catchable; the async/stream paths — the dominant server pattern — are not.)
Proof of Concept
// Build a tar whose single entry has a GNU-L long path of ~12,000 "a/" segments (~26 KB),
// gzip it (≈188 bytes), then have a consumer list/extract with member selection:
const tar = require('tar');
await tar.t({ file: 'evil.tar.gz', gzip: true }, ['some-member']); // -> RangeError, process exit
Empirically reproduced on Node v24.18.0 against built dist/commonjs of node-tar 7.5.20: 188-byte gzip → 26,112-byte tar (12,000 segments) → uncaught RangeError: Maximum call stack size exceeded → process exit. A control run with no member-selection list (filter not installed) parses cleanly (exit 0), isolating mapHas as the sole cause.
Attack Chain
- Entry. Attacker crafts a tar with a GNU
L(or PAXx) long-path header whose body is"a/"×~12000 (~26 KB), followed by a normal file entry.- Guard:
maxMetaEntrySizecaps the meta body at 1 MiB (src/parse.ts:241). - Bypass proof: 26 KB ≪ 1 MiB → accepted (verified: 26 KB archive parsed up to the filter).
- Guard:
- Trigger. Victim service calls
tar.t({file},[sel])ortar.x({file,cwd},[sel])(member selection — a documented, common API).- Guard:
Unpack.maxDepth(default 1024) atsrc/unpack.ts:342; decompression-ratio guard. - Bypass proof:
maxDepthlives in[CHECKPATH]on the'entry'event, which fires afterCONSUMEHEADER's filter call — the crash occurs before it (extract exits 1 with default maxDepth).tar.thas no maxDepth. Ratio is ~139× (trivial); no total-bytes cap applies to the uncompressed meta body.
- Guard:
- Sink.
this.filter(entry.path)→mapHasrecurses once per/segment (src/list.ts:39).- Guard: try/catch in
CONSUMEHEADER. - Bypass proof: the only try/catch wraps
new Header(src/parse.ts:179-183); thethis.filter(...)call atsrc/parse.ts:253is outside it. TheRangeErrorpropagates out of the stream write/'data'path → uncaught exception (verified:process.on('uncaughtException')fires; asyncawait+try/catchdoes NOT intercept).
- Guard: try/catch in
- Impact. Node process termination; a 188-byte gzip crashes any consumer that lists/extracts selected members from untrusted archives.
Bypass Evidence
mapHasrecursion is member-name-independent: the crash fires even when the requested members do not match the malicious entry path — the attacker only needs the consumer to use member selection.- Standalone
mapHasoverflows at 20k–30k segments; on the real streaming path (atopwrite → CONSUMECHUNK → CONSUMEHEADER → filter) it crashes at ≤8k segments (finder's ~12k estimate is accurate for the reachable path). - Control (no member list → no filter) parses cleanly (exit 0), isolating
mapHas.
Affected Versions
<= 7.5.20 (npm tar). mapHas present verbatim on tag v7.5.20 (latest GitHub release and npm dist-tag latest); no segment/depth cap in src/list.ts or the CONSUMEHEADER filter path; HEAD == 7.5.20, no unreleased fix.
Suggested Fix
Rewrite mapHas iteratively (walk dirname in a while loop with a segment/visited cap), or enforce a hard path-segment limit in Header/Parser independent of maxMetaEntrySize, applied before any per-entry filter runs.
Dedup Note
Distinct from CVE-2024-28863 / GHSA-f5x3-32g6-qm9j "lack of folders depth validation" (that bounds mkdir recursion during extraction via maxDepth in Unpack[CHECKPATH] on the 'entry' event — a different sink, code path, and fix; runs after the filter and does not apply to tar.t). Also distinct from the PAX NUL/numeric-path crash advisories (improper-input-to-fs / type confusion, not recursion) and the gzip-bomb advisory (resource exhaustion on disk writes). None touch list.ts/filesFilter/mapHas or require member selection.
Reported by zx (Jace) — GitHub: @manus-use
Affected component
The vulnerability is in pkg:npm/tar@7.5.19, found in artifacts pkg:oci/devguard-documentation?repository_url=ghcr.io/l3montree-dev/devguard-documentation&arch=amd64&tag=main-amd64.
Recommended fix
Upgrade to version 7.5.21 or later.
# Update all vulnerable npm packages
npm audit fix
# Update only this package
npm install tar@7.5.21
Additional guidance for mitigating vulnerabilities
Visit our guides on devguard.org
See more details...
Path to component
%%{init: { 'theme':'base', 'themeVariables': {
'primaryColor': '#F3F3F3',
'primaryTextColor': '#0D1117',
'primaryBorderColor': '#999999',
'lineColor': '#999999',
'secondaryColor': '#ffffff',
'tertiaryColor': '#ffffff'
} }}%%
flowchart TD
Your_application(["Your application"]) --- pkg_npm_tar_7_5_19(["pkg:npm/tar\@7.5.19"])
classDef default stroke-width:2px
| Risk Factor | Value | Description |
|---|---|---|
| Vulnerability Depth | 1 |
The vulnerability is in a direct dependency of your project. |
| EPSS | 0.38 % |
The exploit probability is very low. The vulnerability is unlikely to be exploited in the next 30 days. |
| EXPLOIT | Not available |
We did not find any exploit available. Neither in GitHub repositories nor in the Exploit-Database. There are no script kiddies exploiting this vulnerability. |
| CVSS-BE | 7.5 |
- Exploiting this vulnerability significantly impacts availability. |
| CVSS-B | 7.5 |
- The vulnerability can be exploited over the network without needing physical access. - It is easy for an attacker to exploit this vulnerability. - An attacker does not need any special privileges or access rights. - No user interaction is needed for the attacker to exploit this vulnerability. - The impact is confined to the system where the vulnerability exists. - There is a high impact on the availability of the system. |
More details can be found in DevGuard
Interact with this vulnerability
You can use the following slash commands to interact with this vulnerability:
👍 Reply with this to acknowledge and accept the identified risk.
/accept I accept the risk of this vulnerability, because ...
⚠️ Mark the risk as false positive: Use one of these commands if you believe the reported vulnerability is not actually a valid issue.
/component-not-present The vulnerable component is not included in the artifact.
/vulnerable-code-not-present The component is present, but the vulnerable code is not included or compiled.
/vulnerable-code-not-in-execute-path The vulnerable code exists, but is never executed at runtime.
/vulnerable-code-cannot-be-controlled-by-adversary Built-in protections prevent exploitation of this vulnerability.
/inline-mitigations-already-exist The vulnerable code cannot be controlled or influenced by an attacker.
🔁 Reopen the risk: Use this command to reopen a previously closed or accepted vulnerability.
/reopen ...
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Locate the direct npm tar dependency in the project manifest and run npm audit to confirm the reported GHSA. Upgrade tar to 7.5.21 or later, then rerun the audit and verify that this vulnerability is no longer reported.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, node.js
- Domain
- security
- Issue type
- Bug
- Difficulty
- 1/5
- Estimated time
- Under an hour
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 78/100