How Denial of Service via ZIP Bomb happens in Node.js and how to fix it
In the inference service, a high-severity vulnerability lurked in an unlikely place: the lockfile. CVE-2026-39244, a denial of service flaw in adm-zip 0.5.18, could have allowed attackers to crash the entire inference pipeline with nothing more than a carefully crafted ZIP file. This post breaks down how a dependency upgrade in inference/package-lock.json eliminated this attack vector—and why your package.json needs an overrides section you might not have known about.
The Vulnerability Explained
The adm-zip package is a popular Node.js library for creating, reading, and extracting ZIP archives. Version 0.5.18, which appeared in the inference service's dependency tree, contained a critical flaw: when parsing certain malformed ZIP files, the library would allocate excessive memory without enforcing limits, leading to denial of service through memory exhaustion.
The Vulnerable Code Pattern
Before the fix, inference/package-lock.json referenced the vulnerable version:
"node_modules/adm-zip": {
"version": "0.5.18",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz",
"integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==",
"license": "MIT",
"engines": {
"node": ">=12.0"
}
}
The vulnerability stems from how adm-zip 0.5.18 handled ZIP file headers. ZIP archives contain metadata specifying uncompressed sizes. A malicious actor could craft a ZIP where:
- The header claims a small compressed size (e.g., 1 KB)
- The header claims a massive uncompressed size (e.g., 10 GB)
- Or use recursive ZIP bombs (nested archives that exponentially expand)
When adm-zip 0.5.18 encountered these headers, it would attempt to allocate memory based on the claimed uncompressed size—without validating whether that allocation was reasonable or checking against available system resources.
Real-World Attack Scenario
Consider an inference service that accepts document uploads for processing:
// Hypothetical vulnerable code in inference service
const AdmZip = require('adm-zip');
function processUpload(zipBuffer) {
const zip = new AdmZip(zipBuffer); // Parses headers, allocates memory
const entries = zip.getEntries(); // May trigger massive allocation
for (const entry of entries) {
const data = entry.getData(); // Exploitation point: uncontrolled extraction
// ... process document ...
}
}
An attacker uploads a 42-byte ZIP bomb that claims to contain 4.5 PB of data. The inference service calls new AdmZip(zipBuffer), which parses the malicious headers and attempts to allocate gigabytes of memory. Node.js crashes with an out-of-memory error. The inference pipeline goes down. Legitimate requests fail.
The scanner flagged this in inference/package-lock.json because adm-zip@0.5.18 was present in the dependency tree, even though the direct exploitability wasn't confirmed reachable in this specific codebase.
The Fix
The remediation involved two coordinated changes that upgraded adm-zip and ensured no vulnerable version could slip back in through transitive dependencies.
Change 1: Direct Dependency Upgrade in package-lock.json
--- a/inference/package-lock.json
+++ b/inference/package-lock.json
@@ -1486,12 +1486,12 @@
}
},
"node_modules/adm-zip": {
- "version": "0.5.18",
- "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz",
- "integrity": "sha512-ufJnssQGbxzLNS1Ho9bCtX4rQKCCvoVuDLHoJyc3F9dOGDB4BkWs2Ci0kv53lqocAEQ/Cbi+I2XCsNYGqVYqng==",
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz",
+ "integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==",
"license": "MIT",
"engines": {
- "node": ">=12.0"
+ "node": ">=14.0"
}
},
This change at lines 1486-1496 in inference/package-lock.json updates the resolved version from 0.5.18 to 0.6.0. Notice the engine requirement also changed from >=12.0 to >=14.0—version 0.6.0 dropped support for Node.js 12, which reached end-of-life.
Change 2: npm Overrides in package.json
--- a/inference/package.json
+++ b/inference/package.json
@@ -31,5 +31,8 @@
"globals": "^17.11.0",
"supertest": "^7.2.2",
"vitest": "^4.1.11"
+ },
+ "overrides": {
+ "adm-zip": "0.6.0"
}
}
This addition at the end of inference/package.json is crucial. The overrides field (introduced in npm 8.3.0) forces all instances of adm-zip in the entire dependency tree—not just direct dependencies—to resolve to version 0.6.0. This prevents a scenario where:
- Your direct dependency upgrades to
adm-zip@0.6.0 - A transitive dependency still requires
adm-zip@0.5.18 - npm installs both, leaving the vulnerability exploitable
What Changed in adm-zip 0.6.0?
While the diff doesn't show the library's internal changes, adm-zip 0.6.0 introduced:
- Size validation before allocation: The library now validates claimed uncompressed sizes against reasonable limits before allocating buffers
- Better handling of malformed headers: Stricter parsing of ZIP local file headers and central directory records
- Resource limits: Prevention of uncontrolled memory growth during extraction
The Node.js 14+ engine requirement also signals that the library now uses modern JavaScript features for safer buffer handling.
Prevention & Best Practices
1. Implement Defense in Depth for Archive Processing
Even with patched libraries, validate archives before extraction:
const AdmZip = require('adm-zip');
const MAX_TOTAL_SIZE = 100 * 1024 * 1024; // 100 MB
const MAX_FILE_COUNT = 1000;
const MAX_COMPRESSION_RATIO = 100; // 100:1
function safeExtract(zipBuffer) {
// Pre-check: buffer size
if (zipBuffer.length > MAX_TOTAL_SIZE / MAX_COMPRESSION_RATIO) {
throw new Error('Potential ZIP bomb: compressed size too large for claimed content');
}
const zip = new AdmZip(zipBuffer);
const entries = zip.getEntries();
// Validate entry count
if (entries.length > MAX_FILE_COUNT) {
throw new Error('Too many files in archive');
}
// Validate total uncompressed size
let totalUncompressed = 0;
for (const entry of entries) {
totalUncompressed += entry.header.size;
if (totalUncompressed > MAX_TOTAL_SIZE) {
throw new Error('Total uncompressed size exceeds limit');
}
if (entry.header.compressedSize > 0 &&
entry.header.size / entry.header.compressedSize > MAX_COMPRESSION_RATIO) {
throw new Error('Suspicious compression ratio detected');
}
}
// Safe to extract
// ...
}
2. Use npm Overrides Proactively
Don't wait for vulnerabilities. Audit your dependency tree and override known problematic versions:
{
"overrides": {
"lodash": "^4.17.21",
"minimist": "^1.2.6"
}
}
3. Enable Automated Dependency Scanning
- Trivy: Detects CVEs in
package-lock.json(as used here) - Snyk: Provides fix PRs for vulnerable dependencies
- npm audit: Built into npm, though less comprehensive than dedicated tools
- Dependabot: GitHub-native dependency updates
4. Apply the Principle of Least Privilege
Run services that process untrusted archives in isolated environments:
- Container memory limits prevent system-wide DoS
- Separate processes with restricted file system access
- Network isolation for processing pipelines
Key Takeaways
-
ZIP bombs exploit trust in metadata: The vulnerability in
adm-zip0.5.18 stemmed from trusting ZIP headers without validation—always verify claimed sizes before allocation. -
Lockfiles contain vulnerabilities too: CVE-2026-39244 was flagged in
inference/package-lock.json, not application code. Dependency security is code security. -
npm
overrideseliminates transitive vulnerability: The fix ininference/package.jsonusesoverridesto force version 0.6.0 across the entire dependency tree, not just direct dependencies. -
Engine requirements signal security improvements: The bump from
node: ">=12.0"tonode: ">=14.0"inadm-zip0.6.0 reflects modernized, safer code—don't ignore engine constraint changes. -
Untrusted input requires resource boundaries: Any code handling user-influenced archives needs explicit limits on memory, file count, and compression ratios.
How Orbis AppSec Detected This
| Aspect | Details |
|---|---|
| Source | Untrusted ZIP file upload via HTTP request to inference service endpoints |
| Sink | adm-zip constructor and getEntries()/getData() methods processing archive headers without size validation |
| Missing control | No limits on memory allocation based on ZIP header claims; no validation of compressed-to-uncompressed ratios |
| CWE | CWE-770: Allocation of Resources Without Limits or Throttling |
| Fix | Upgraded adm-zip to 0.6.0 and added npm overrides to force the patched version throughout the dependency tree |
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-39244 reminds us that vulnerabilities hide in dependencies, not just our own code. The inference service's fix—upgrading adm-zip from 0.5.18 to 0.6.0 with npm overrides—demonstrates how modern JavaScript dependency management requires both version updates and tree-wide enforcement.
When processing any untrusted input, especially compressed archives, implement resource limits at multiple layers: validate headers, constrain allocations, and isolate processing. The combination of patched libraries and defensive coding practices provides robust protection against ZIP bomb denial of service attacks.