The Scenario: A Tiny File That Can Take Down Your Server
Imagine your Node.js application receives a .tar.gz upload — perhaps a plugin bundle, a build artifact, or a data import file. The file is only 50 KB. Your code hands it to the tar library and begins extracting. Seconds later, your server is out of memory and the process crashes. No payload executed, no data was stolen — but your service is offline. This is the essence of a gzip bomb attack, and it is exactly what CVE-2026-59873 enables in node-tar versions prior to 7.5.19.
This post walks through the specific vulnerability, the exact code change that fixes it, and what you can do to prevent similar issues in your own projects.
The Vulnerability Explained
What Is a Gzip Bomb?
A gzip bomb (also called a decompression bomb) exploits the fact that gzip compression ratios can be extreme. A legitimate file might compress 10:1. A crafted gzip bomb can achieve ratios of 1,000,000:1 or higher — a 50 KB file that decompresses to 50 GB. When a library streams decompressed bytes into memory without any size ceiling, the host process will consume all available RAM and either crash or be killed by the OS.
The Vulnerable Code Path in node-tar 7.5.16
The vulnerability lives in how node-tar processes the gzip layer of a .tar.gz archive. In versions up to and including 7.5.16, the library pipes the gzip decompression stream directly into the tar parser without enforcing a maximum decompressed byte count. The relevant behavior is:
[compressed .tar.gz input]
↓
zlib.createGunzip() ← no size limit enforced here
↓
tar entry parser ← receives unbounded byte stream
↓
file system / memory ← exhausted by bomb payload
Because there is no guard between the Gunzip stream and the tar parser, a crafted archive can instruct the decompressor to emit an arbitrarily large output. The application's process — in this case a Netlify-hosted Node.js app — has no opportunity to intervene before memory is consumed.
The vulnerable version was locked in package-lock.json:
// BEFORE — vulnerable
"node_modules/tar": {
"version": "7.5.16",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz",
"integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w=="
}
And the package.json did not pin tar as a direct dependency at all — it arrived transitively through netlify-cli ^26.1.0, meaning the vulnerable version could silently persist across installs.
Real-World Attack Scenario
Consider this application's use of netlify-cli as a production dependency. The CLI uses tar internally to pack and unpack deployment bundles. An attacker who can influence the content of a deployment archive — for example, through a compromised CI pipeline, a malicious npm package in the dependency tree, or a crafted response from a spoofed registry — could supply a .tar.gz gzip bomb. When netlify-cli (and by extension node-tar 7.5.16) attempts to extract it, the decompression loop runs without bounds, consuming all available memory on the build or runtime server.
The impact is a complete process crash — a Denial of Service that could take down a production deployment pipeline or a running service, rated CRITICAL severity.
The Fix
Two-File Change: Direct Dependency Pin + Lock File Update
The fix involves two coordinated changes:
1. package.json — Pin tar as a direct dependency
// BEFORE
"dependencies": {
"@netlify/database": "^1.1.0",
"@netlify/identity": "^1.2.0",
"netlify-cli": "^26.1.0",
"postgres": "^3.4.9"
}
// AFTER
"dependencies": {
"@netlify/database": "^1.1.0",
"@netlify/identity": "^1.2.0",
"netlify-cli": "^26.1.0",
"postgres": "^3.4.9",
"tar": "^7.5.19" // ← explicit floor on the safe version
}
Adding tar as an explicit direct dependency ensures that npm's resolution algorithm will always select at least version 7.5.19, even when netlify-cli or another transitive dependency would otherwise resolve to an older version.
2. package-lock.json — Updated resolution and integrity hash
// BEFORE
"node_modules/tar": {
"version": "7.5.16",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.16.tgz",
"integrity": "sha512-56adEpPMouktRlBLXiaYFFzZ/3+JXa8P9n7WbR+ibIjtviN55mEaOkiysCnPnWm+7kkui1Dn8J9l+g6zV8731w=="
}
// AFTER
"node_modules/tar": {
"version": "7.5.19",
"resolved": "https://registry.npmjs.org/tar/-/tar-7.5.19.tgz",
"integrity": "sha512-4LeEWl96twnS2Q7Bz4MGqgazLqO+hJN63GZxXoIqh1T3VweYD997gbU1ItNsQafqqXTXd5WFyFdReLtwvRBNiw=="
}
The updated integrity hash (sha512-4LeEWl96...) cryptographically binds the installed package to the exact bytes of version 7.5.19 — any tampering or substitution will cause npm ci to fail, providing supply-chain protection.
What Changed Inside node-tar 7.5.19
Version 7.5.19 introduces decompression size guards in the gzip pipeline. The patched library tracks the total number of bytes emitted by the Gunzip stream and aborts extraction with an error when the output exceeds a configurable (and sane default) threshold. This means a gzip bomb triggers a controlled error rather than unbounded memory growth:
[compressed .tar.gz input]
↓
zlib.createGunzip()
↓
[size counter: bytes emitted so far] ← NEW in 7.5.19
↓ if > maxDecompressedSize → throw ERR_TAR_DECOMPRESSION_BOMB
tar entry parser
↓
file system / memory ← protected
The application's error handling can now catch this error gracefully rather than being terminated by OOM.
Prevention & Best Practices
1. Pin Transitive Security-Critical Dependencies
When a transitive dependency has a known CVE, do not rely solely on the upstream package to update. Add an explicit entry in your package.json dependencies (or overrides) to force a safe minimum version, exactly as this fix does with "tar": "^7.5.19".
// Use overrides for packages you don't directly import
"overrides": {
"tar": "^7.5.19"
}
2. Enforce Archive Size Limits at the Application Layer
Even with a patched library, consider wrapping archive processing with application-level guards:
const MAX_UNCOMPRESSED_BYTES = 500 * 1024 * 1024; // 500 MB
async function safeExtract(archivePath, destination) {
let totalBytes = 0;
await tar.extract({
file: archivePath,
cwd: destination,
onentry: (entry) => {
totalBytes += entry.size;
if (totalBytes > MAX_UNCOMPRESSED_BYTES) {
throw new Error('Archive exceeds maximum allowed size');
}
}
});
}
3. Never Process Untrusted Archives Without Timeouts
Wrap archive extraction in a timeout to limit the blast radius of any resource exhaustion attack:
const extractWithTimeout = (archivePath, dest, timeoutMs = 30_000) =>
Promise.race([
tar.extract({ file: archivePath, cwd: dest }),
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Extraction timed out')), timeoutMs)
)
]);
4. Integrate Dependency Scanning in CI
The scanner that caught this vulnerability was Trivy, which compares package-lock.json entries against the NVD and GitHub Advisory Database. Add it to your CI pipeline:
# .github/workflows/security.yml
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
severity: 'HIGH,CRITICAL'
exit-code: '1'
5. Security Standards
- OWASP A06:2021 — Vulnerable and Outdated Components: Regularly audit and update third-party dependencies.
- CWE-400 — Uncontrolled Resource Consumption: Ensure all resource-consuming operations (decompression, parsing, network I/O) have explicit upper bounds.
Key Takeaways
- Transitive dependencies carry real risk:
tarwas not inpackage.jsonat all before this fix — it arrived throughnetlify-cli. That didn't make CVE-2026-59873 any less exploitable. - Gzip bombs bypass file-size checks: Checking the size of the
.tar.gzfile before extraction is useless against this attack. Only decompression-time limits (added in node-tar 7.5.19) are effective. - Pinning a direct dependency overrides transitive versions: Adding
"tar": "^7.5.19"todependenciesinpackage.jsonis the correct npm idiom to force a minimum safe version regardless of whatnetlify-clirequests. - Integrity hashes in
package-lock.jsonmatter: The newsha512hash for 7.5.19 ensures thatnpm ciwill reject any attempt to substitute a different (potentially malicious) build of the package. - Static analysis scanners catch CVEs before attackers do: Trivy flagged this pattern in the lock file automatically — demonstrating the value of integrating dependency scanning into every build pipeline.
How Orbis AppSec Detected This
- Source: The
bun.lock/package-lock.jsondependency manifest, which recordednode-tarversion7.5.16as the resolved version of thetarpackage — a version known to accept unbounded gzip decompression input. - Sink: The
tarlibrary's internal gzip decompression pipeline (zlib.createGunzip()stream innode_modules/tar) receives data from any caller that invokestar.extract()ortar.list()on a user-influenced or remotely fetched.tar.gzfile. - Missing control: No maximum decompressed byte count was enforced in the
Gunzip→ tar-parser pipeline in version 7.5.16, allowing unbounded memory growth. - CWE: CWE-400 — Uncontrolled Resource Consumption
- Fix: The
tardependency was upgraded from7.5.16to7.5.19in bothpackage.json(as an explicit direct dependency pin) andpackage-lock.json(with an updated integrity hash), ensuring the patched version with decompression size guards is installed.
Orbis AppSec automatically detected this vulnerability and opened a pull request with the fix. Try Orbis AppSec on your repositories to find and fix issues like this automatically.
Conclusion
CVE-2026-59873 is a sharp reminder that compressed data is not safe data. The tar library's failure to cap decompressed output in version 7.5.16 means a single malicious archive — small enough to pass file-size checks — can silently kill a Node.js process. The fix is surgical: upgrade to 7.5.19, pin the version explicitly in package.json so it cannot regress through transitive resolution, and validate the lock file's integrity hash. Pair that with CI-integrated scanning tools like Trivy, and this class of vulnerability becomes detectable and patchable before it ever reaches production.