GHSA-gvwx-54wh-qm9j node-tar: Uncaught Exception DoS via NUL byte in PAX path/linkpath records
Summary`node-tar` strips trailing `NUL` bytes from long-name (`L`) and long-linkpath (`K`) GNU extended headers but does **not** apply the same sanitization to equivalent fields delivered via PAX (`x` typeflag) extended headers. A PAX record of the form `path=visible.txt\x00hidden.txt` is parsed verbatim into `entry.path` and flows into `fs.lstat()` / `fs.open()`, which Node.js core rejects with `ERR_INVALID_ARG_VALUE`. The throw originates inside an `FSReqCallback` async chain that is **not** wrapped by the consumer's `await/try-catch` around `tar.x()` — it surfaces as `uncaughtException` and terminates the process.This is a remote denial-of-service primitive against any process that extracts attacker-supplied tarballs through `tar.x` / `tar.extract` / `tar.t` / `tar.Parser`, even when the consumer follows the documented `try/catch` error-handling pattern.A secondary parser-differential (CWE-436) exists because `tar(1)`, `bsdtar`, and Python `tarfile` truncate the path at the first `NUL` (yielding `visible.txt`) while node-tar retains the full string. A validator that pre-scans a tarball with one tool and extracts with the other is bypassed.Root causeVulnerable sink — `src/pax.ts:157-183`PAX KV records flow through `parseKVLine`. The value half (`v`) is assigned directly to the result object with no sanitization for embedded NUL bytes:The PAX record body is length-prefixed, so the parser knows the exact byte boundary — but it never checks whether the value half between `=` and `\n` contains `NUL`. The result is consumed by `Header` / `ReadEntry`, where `entry.path` and `entry.linkpath` carry the embedded NUL all the way to `fs.lstat()`.Correctly-patched cousin sink — `src/parse.ts:375-388`The equivalent code path for GNU L/K long-headers **does** strip NUL bytes:The `parse.ts` fix is the maintainer's own acknowledgement that path strings on this codepath must be NUL-stripped before reaching `fs.*`. The PAX path produces the identical primitive but bypasses the guard.Downstream blast radius`entry.path` and `entry.linkpath` are consumed in:`src/unpack.ts` → `fs.lstat`, `fs.open`, `fs.symlink`, `fs.link`, `fs.mkdir``src/list.ts` (no crash — listing tolerates NUL in strings)Any consumer of the `ReadEntry` event that calls `path.join()` / `fs.*` on `entry.path`The crash fires inside the FSReqCallback Node-internal async machinery, **outside** the user's `await tar.x(...)` Promise rejection boundary.Proof of ConceptArtifacts`poc-null-byte-crash.tar` — 3072 bytes — PAX `path=visible.txt\x00hidden.txt``poc-null-linkpath-crash.tar` — 2560 bytes — PAX `linkpath=target\x00garbage` (symlink target sink)`poc1-pax-prefix.py` — minimal PAX-header builder (Python 3, no deps)Tarball generator (minimal repro — Python 3)ReproductionObserved output (verified 2026-06-23 against `tar@7.5.16`)The exception bypasses the user's `try { await tar.x(...) } catch (e) { ... }` block and lands in the global `uncaughtException` handler. In a typical server without that handler, the process exits.ImpactDirect: remote DoSAny service that ingests attacker-supplied tarballs via node-tar inherits a one-tarball-kills-the-process primitive. Realistic deployments where this is reachable without user interaction:npm registry tarball ingestion and downstream mirrorsGitHub Actions cache restore (`actions/cache`, `actions/setup-*` extracting toolchains)Container image build pipelines that unpack layer tarballs through node toolingBackup-restore services accepting user uploadsCI artifact processors and badge generatorsStatic-site / Docusaurus / Next.js build runners that fetch and extract dep tarballsCloud functions that auto-extract uploaded archivesA correctly-coded consumer that does:does not catch this throw. The Node process dies and (depending on the supervisor) the worker may take time to respawn or never respawn if it dies during boot.Secondary: parser-differential validator bypass (CWE-436)| Tool                       | Result for `path=visible.txt\x00hidden.txt` |
|----------------------------|----------------------------------------------|
| GNU tar (`tar -tvf`)       | Lists `visible.txt` (truncated at NUL)      |
| `bsdtar -tvf`              | Lists `visible.txt` (truncated at NUL)      |
| Python `tarfile.list()`    | Lists `visible.txt\x00hidden.txt` (raw)     |
| node-tar `tar.t({file})`   | Emits raw NUL-bearing path (no crash)       |
| node-tar `tar.x({file})`   | **Crashes** (uncaught throw)                |A pre-flight validator using GNU tar or bsdtar will see a benign filename; the subsequent node-tar extraction blows up. This is exploitable against any architecture that lists-and-validates-then-extracts.Suggested patchMatch the long-name handler in `parse.ts` — strip everything from the first NUL onward in `parseKVLine` value parsing:This matches `src/parse.ts:379` and `src/parse.ts:386` and closes both `path` and `linkpath` sinks in one change.A defense-in-depth follow-up: add an explicit `assert(!v.includes('\0'))` (or fail-soft `return set`) at the top of `parseKVLine` so malformed PAX records that *aren't* path/linkpath also can't smuggle NUL into other unanticipated consumers (e.g. third-party readers of `entry.header.atime` Date objects constructed from `Number(v)` where `v` had embedded NUL).
CVSS v3.1 score: 5.3