The Hidden Bomb in Your ZIP Parser
Every time a Node.js application calls adm-zip to open an archive uploaded by a user, it implicitly trusts the numbers embedded in that archive's headers. How many bytes should this entry be? The ZIP file says so. How much memory should be pre-allocated for decompression? The ZIP file says so. In adm-zip versions up to and including 0.5.x, that blind trust was never challenged — and CVE-2026-39244 is the result.
This post walks through exactly what went wrong, how the fix in version 0.6.0 closes the door, and what every Node.js developer handling user-supplied archives should do right now.
The Vulnerability Explained
What adm-zip Does (and Where It Goes Wrong)
adm-zip is one of the most widely used pure-JavaScript ZIP libraries on npm. It reads ZIP archives, parses their central directory and local file headers, and exposes entries for reading or extraction. The central directory of a ZIP file contains metadata for every entry: compressed size, uncompressed size, file name length, extra field length, and so on.
In adm-zip 0.5.x (the version locked in package-lock.json before this fix), those size fields were used directly to allocate buffers without adequate bounds validation. An attacker who controls the ZIP file can set an uncompressed-size field to, say, 0xFFFFFFFF (≈ 4 GB) while the actual compressed data is only a few hundred bytes. When adm-zip reads that field and calls something equivalent to:
// Simplified illustration of the vulnerable pattern in adm-zip 0.5.x
const buf = Buffer.alloc(entry.header.size); // size comes straight from the ZIP header
…the Node.js process immediately attempts to allocate that many bytes on the heap. On a server with 2 GB of RAM, a single such request can exhaust memory and either crash the process with an out-of-memory error or trigger aggressive garbage collection that renders the service unresponsive.
The package-lock.json before the fix recorded:
"node_modules/adm-zip": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
"engines": {
"node": ">=12.0"
}
}
Attack Scenario
Consider a document-processing service that accepts ZIP uploads and uses adm-zip to enumerate the contents before handing files off to a downstream processor:
const AdmZip = require('adm-zip');
app.post('/upload', upload.single('archive'), (req, res) => {
const zip = new AdmZip(req.file.buffer); // <-- attacker controls this buffer
const entries = zip.getEntries();
entries.forEach(entry => {
const data = entry.getData(); // triggers the dangerous allocation
// ... process data
});
res.json({ count: entries.length });
});
An attacker crafts a ZIP file with a single entry whose uncompressed-size header field is set to several gigabytes. The file itself is tiny — easily under any upload-size limit — but entry.getData() internally calls into adm-zip's buffer allocation logic with the attacker-supplied size. The result: the Node.js event loop freezes or the process dies, taking every concurrent user's session with it.
Because this requires only an HTTP POST with a small file, it is trivially automatable and requires no authentication if the upload endpoint is public.
The Fix
Two-File Change, One Clear Goal
The fix touches exactly two files: package-lock.json (to update the resolved version and integrity hash) and package.json (to add an overrides block that prevents transitive dependencies from pulling in any older version).
package-lock.json — Version and Integrity Update
"node_modules/adm-zip": {
- "version": "0.5.16",
- "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
- "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
+ "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"
}
}
adm-zip 0.6.0 introduces proper validation of size fields before any buffer allocation takes place. The library now checks that the declared uncompressed size is consistent with the actual compressed payload and rejects entries whose headers claim unreasonable sizes. This directly eliminates the attack vector described above.
The node engine bump from >=12.0 to >=14.0 is also meaningful: Node.js 14 introduced improved Buffer APIs and memory-safety improvements that the new adm-zip release takes advantage of.
package.json — The overrides Block
+ "overrides": {
+ "adm-zip": "0.6.0"
+ }
This is the second, equally important part of the fix. Without it, a transitive dependency — some other package in the tree that lists adm-zip as its own dependency — could still resolve to 0.5.x. The npm overrides field (introduced in npm 8.3) forces every consumer in the dependency tree to use exactly 0.6.0, regardless of what version range they specify. This closes the gap between "we upgraded our direct dependency" and "we actually run the patched code everywhere."
Why Both Changes Are Necessary
| Change | What it protects |
|---|---|
package-lock.json version bump |
Ensures npm ci installs the patched library for direct usage |
package.json overrides block |
Ensures no transitive dependency can silently re-introduce 0.5.x |
Prevention & Best Practices
1. Never Trust Archive Metadata
When parsing any container format (ZIP, TAR, JAR, DOCX), treat all size and count fields as untrusted input. Validate them against configurable maximums before allocating memory or opening file handles:
const MAX_UNCOMPRESSED_SIZE = 512 * 1024 * 1024; // 512 MB
zip.getEntries().forEach(entry => {
if (entry.header.size > MAX_UNCOMPRESSED_SIZE) {
throw new Error(`Entry ${entry.entryName} exceeds size limit`);
}
const data = entry.getData();
});
2. Pin Transitive Dependencies with overrides
The npm overrides field (or Yarn's resolutions) is an underused but powerful tool. Any time a security scanner flags a transitive dependency, add an override so future npm install runs cannot regress:
"overrides": {
"adm-zip": "0.6.0"
}
3. Integrate Dependency Scanning in CI
Tools like Trivy, npm audit, and Snyk can flag vulnerable dependency versions before they reach production. Trivy rule CVE-2026-39244 is exactly what caught this issue. Add a step like the following to your CI pipeline:
- name: Scan dependencies
run: trivy fs --exit-code 1 --severity HIGH,CRITICAL .
4. Set Upload Size and Entry Count Limits
Even with a patched library, apply defense-in-depth at the application layer:
- Reject archives larger than a reasonable threshold (e.g., 50 MB).
- Limit the number of entries processed (e.g., 1,000 max).
- Consider processing archives in a sandboxed worker or container with a memory cap.
5. Monitor for Relevant CWEs
This vulnerability maps to CWE-400: Uncontrolled Resource Consumption and CWE-789: Memory Allocation with Excessive Size Value. Review your codebase for any pattern where a size or length value derived from external input is passed directly to Buffer.alloc(), new Array(), or similar allocation calls.
OWASP Reference: OWASP A06:2021 – Vulnerable and Outdated Components
Key Takeaways
- adm-zip 0.5.x trusts ZIP header size fields without bounds-checking — a single malicious archive can exhaust all available Node.js heap memory.
- Upgrading to 0.6.0 is not optional if you process user-supplied ZIPs — the vulnerability is trivially exploitable with a tiny, specially crafted file.
- The
overridesblock inpackage.jsonis as important as the version bump — without it, transitive dependencies can silently re-introduce the vulnerable 0.5.x release. - The Node.js engine requirement changed from
>=12.0to>=14.0— verify your runtime version before deploying the patched library. - Trivy's static dependency scan caught this before runtime — integrating scanner rules like
CVE-2026-39244into CI gives you a safety net that code review alone cannot provide.
How Orbis AppSec Detected This
- Source: A user-supplied ZIP archive passed to
new AdmZip(buffer)ornew AdmZip(filePath)in any route or service that accepts file uploads. - Sink: adm-zip's internal buffer allocation logic, which calls
Buffer.alloc(entry.header.size)(or equivalent) using the unvalidatedsizefield from the ZIP central directory — resolved via thenode_modules/adm-zipentry inpackage-lock.json. - Missing control: No upper-bound validation on the declared uncompressed size before memory allocation; no rejection of entries with implausible size ratios.
- CWE: CWE-400 — Uncontrolled Resource Consumption (also related: CWE-789 — Memory Allocation with Excessive Size Value).
- Fix: Upgraded
adm-zipfrom0.5.16to0.6.0inpackage-lock.jsonand added anoverridesentry inpackage.jsonto enforce the patched version across the entire 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 is a sharp reminder that ZIP files are not passive data containers — they are structured documents whose metadata fields are fully under attacker control. adm-zip 0.5.x placed unconditional trust in those fields, and the consequence is a straightforward path to memory exhaustion and service outage. The fix is surgical: two files changed, one version bumped, one overrides block added. But the lesson extends well beyond adm-zip — any code that allocates memory based on a size field from an external source must validate that field before acting on it.
Keep your dependencies current, scan them automatically, and never let an archive tell your application how much memory to use.
References
- CWE-400: Uncontrolled Resource Consumption
- CWE-789: Memory Allocation with Excessive Size Value
- OWASP A06:2021 – Vulnerable and Outdated Components
- OWASP Input Validation Cheat Sheet
- adm-zip on npm (official package page)
- npm overrides documentation
- Semgrep rules for unsafe buffer allocation
- fix: upgrade adm-zip to 0.6.0 (CVE-2026-39244)