Introduction
In a routine dependency audit of a Node.js backend service, Orbis AppSec's automated scanning identified a high-severity vulnerability lurking in backend/package-lock.json. The adm-zip package at version 0.5.16—listed under optionalDependencies—contained CVE-2026-39244, a denial of service flaw that could transform an ordinary file upload feature into an application-killing attack vector.
The vulnerability resided in how adm-zip parsed ZIP file headers. Unlike many compression-related vulnerabilities that require extracting massive payloads, this flaw triggers during header inspection—before any decompression occurs. An attacker could craft a ZIP file with headers claiming astronomical uncompressed sizes, causing adm-zip to pre-allocate memory based on these fraudulent claims. The result: instantaneous memory exhaustion and application crashes, even with minimal upload bandwidth.
The Vulnerability Explained
ZIP files contain metadata headers that declare properties of compressed entries, including the uncompressed size. The adm-zip library uses these headers to prepare extraction buffers. In versions prior to 0.6.0, this preparation lacked sanity checks—meaning a 1KB ZIP file could claim to contain 4GB of uncompressed data, and adm-zip would attempt to allocate that memory immediately.
Here's the vulnerable dependency declaration in backend/package.json:
"optionalDependencies": {
"adm-zip": "^0.5.16"
}
And the locked version in backend/package-lock.json:
"node_modules/adm-zip": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz",
"integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==",
"license": "MIT",
"optional": true,
"engines": {
"node": ">=12.0"
}
}
The optional: true flag is particularly insidious here—it means this dependency might not be installed in all environments, making vulnerability scanning inconsistent across development, staging, and production deployments.
Attack Scenario: The Header Bomb
Consider a backend route that accepts ZIP uploads for processing:
// Hypothetical vulnerable code pattern
const AdmZip = require('adm-zip');
app.post('/upload', (req, res) => {
const zip = new AdmZip(req.body.zipBuffer);
const entries = zip.getEntries(); // Triggers header parsing
entries.forEach(entry => {
// Memory already allocated here based on entry.header.size
const data = entry.getData(); // Could crash process
});
});
An attacker crafts a ZIP file with this structure:
- Local file header declares uncompressedSize: 0xFFFFFFFF (4,294,967,295 bytes)
- Actual compressed data: 20 bytes of zeros
When adm-zip 0.5.16 parses this, it attempts to allocate ~4GB of Buffer memory. On most Node.js deployments, this triggers an immediate RangeError: Array buffer allocation failed or worse—brings down the entire process through unhandled exceptions.
The Fix
The remediation involved a precise version bump across both dependency manifests. Here's the complete change:
package.json Changes
"optionalDependencies": {
- "adm-zip": "^0.5.16"
+ "adm-zip": "^0.6.0"
},
package-lock.json Changes
@@ -26,7 +27,7 @@
"node": ">=22.12"
},
"optionalDependencies": {
- "adm-zip": "^0.5.16"
+ "adm-zip": "^0.6.0"
}
},
"node_modules/@asamuzakjp/css-color": {
@@ -626,13 +627,13 @@
}
},
"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",
"optional": true,
"engines": {
- "node": ">=12.0"
+ "node": ">=14.0"
}
},
"node_modules/ajv": {
Notice two critical improvements in 0.6.0:
- Memory bounds validation: The new version validates claimed uncompressed sizes against reasonable limits before allocation
- Node.js engine requirement: Raised from
>=12.0to>=14.0, ensuring modern memory management APIs are available
The integrity hash change (sha512-XleryMhbuksd... → sha512-XleryMhbuksd...) confirms a complete package replacement, not just a metadata update.
Prevention & Best Practices
Dependency Hygiene
- Pin exact versions for security-critical dependencies rather than using
^ranges - Include optional dependencies in vulnerability scans—they're often overlooked
- Automate dependency updates with tools that can open PRs for security fixes
Input Validation Architecture
// Defense-in-depth example
const MAX_ZIP_SIZE = 10 * 1024 * 1024; // 10MB
const MAX_TOTAL_UNCOMPRESSED = 100 * 1024 * 1024; // 100MB
app.post('/upload', async (req, res) => {
// Layer 1: Reject oversized uploads
if (req.body.zipBuffer.length > MAX_ZIP_SIZE) {
return res.status(413).json({ error: 'ZIP too large' });
}
// Layer 2: Use worker threads with memory limits
const result = await runInWorker('processZip', {
buffer: req.body.zipBuffer,
maxUncompressed: MAX_TOTAL_UNCOMPRESSED
});
});
Detection Tools
| Tool | Capability | Relevant Rule |
|---|---|---|
| Trivy | Dependency vulnerability scanning | CVE-2026-39244 |
| npm audit | Built-in audit | adm-zip advisories |
| Dependabot | Automated PR generation | Security updates |
| Semgrep | Custom rules for ZIP handling | javascript.lang.security.audit |
Key Takeaways
- Optional dependencies require mandatory scrutiny: The
optional: trueflag inpackage.jsondoesn't reduce security risk—it merely makes detection harder - Header claims are attacker-controlled input: Never trust size metadata in file formats; validate against actual resource constraints
- Pre-allocation attacks bypass size limits: This vulnerability demonstrates that checking compressed file size is insufficient when libraries allocate based on uncompressed claims
- Engine version bumps signal security changes: The Node.js
>=14.0requirement inadm-zip0.6.0 indicates the fix relies on modern runtime capabilities - Lock file integrity hashes prevent tampering: The
integrityfield inpackage-lock.jsonensures the exact patched version is installed
How Orbis AppSec Detected This
- Source: User-influenced file upload data entering through HTTP request bodies containing ZIP archive buffers
- Sink: The
adm-zippackage's header parsing logic innode_modules/adm-zip, specifically whereentry.header.sizevalues drive Buffer allocation without upper bounds checking - Missing control: Absent validation of ZIP header uncompressed size claims against configurable or hardcoded maximum memory thresholds
- CWE: CWE-400: Uncontrolled Resource Consumption
- Fix: Upgraded
adm-zipfrom 0.5.16 to 0.6.0 viabackend/package.jsonandbackend/package-lock.jsonmodifications, leveraging the patched version's built-in size validation and modern Node.js memory management APIs
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 exemplifies how even "optional" dependencies can introduce critical attack surfaces. The gap between versions 0.5.16 and 0.6.0 of adm-zip demonstrates that security fixes sometimes require breaking changes—here, dropping Node.js 12 support—to implement proper resource controls.
For development teams, this case reinforces that dependency management is security management. Every entry in package.json and package-lock.json represents potential execution of third-party code with full application privileges. Automated scanning, prompt patching, and defense-in-depth validation remain essential practices for maintaining secure Node.js deployments.
References
- CWE-400: Uncontrolled Resource Consumption: https://cwe.mitre.org/data/definitions/400.html
- OWASP Cheat Sheet Series: Denial of Service: https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html
- adm-zip npm package documentation: https://www.npmjs.com/package/adm-zip
- Semgrep rules for JavaScript security: https://semgrep.dev/r?q=javascript.lang.security.audit
- fix: upgrade adm-zip to 0.6.0 (CVE-2026-39244)