How Denial of Service via ZIP Parsing Happens in Node.js and How to Fix It
Vulnerability at a Glance
| Field | Detail |
|---|---|
| CVE | CVE-2026-39244 |
| Severity | High |
| Package | adm-zip < 0.6.0 |
| CWE | CWE-400 – Uncontrolled Resource Consumption |
| Language | JavaScript / Node.js |
| Fixed in | adm-zip 0.6.0 |
Introduction
The dsh-mneme component of this project handles ZIP file processing through the popular adm-zip npm package. A high-severity vulnerability — CVE-2026-39244 — was discovered in adm-zip versions prior to 0.6.0 that allows any party capable of supplying a ZIP file to the application to trigger uncontrolled memory allocation, potentially crashing the Node.js process entirely.
This is not a subtle logic flaw buried deep in application code. It lives in a widely-used third-party library that is a transitive or direct dependency recorded in dsh-mneme/package-lock.json. The fix is a one-version upgrade, but understanding why this class of vulnerability is dangerous — and how ZIP files can be weaponized — is essential knowledge for any Node.js developer working with file uploads or archive processing.
The Vulnerability Explained
How ZIP Parsing Works (and Where It Can Go Wrong)
A ZIP archive is not just a compressed blob. It contains a structured Central Directory at the end of the file, which is a table of metadata entries describing each file inside the archive: its name, compressed size, uncompressed size, offset in the file, and more. A ZIP parser typically reads this directory first to understand what's inside before decompressing anything.
The critical trust boundary is here: the parser must not blindly trust the size fields in the Central Directory. Those fields are written by whoever created the ZIP file. An attacker can craft a ZIP archive where a metadata field claims an entry's uncompressed size is, say, 4 gigabytes — even though the actual compressed data is only a few bytes. If the parser allocates a buffer of that claimed size before reading the compressed data, memory is exhausted instantly.
The Vulnerable Pattern in adm-zip < 0.6.0
In versions of adm-zip prior to 0.6.0, the library's entry-reading logic did not adequately validate the size values declared in ZIP Central Directory headers against the actual file size or configurable limits before allocating output buffers. The vulnerable behavior looks conceptually like this:
// Simplified illustration of the vulnerable pattern in adm-zip < 0.6.0
function readEntry(entry) {
const uncompressedSize = entry.header.size; // Attacker-controlled value
const buffer = Buffer.alloc(uncompressedSize); // Allocated WITHOUT bounds check
// ... decompress into buffer
}
The entry.header.size field comes directly from the ZIP file's metadata. In adm-zip 0.5.18 (the version pinned in dsh-mneme/package-lock.json before this fix), that value was used to allocate a Buffer without first verifying it was reasonable relative to the actual compressed data or any system limit.
Attack Scenario
Consider a dsh-mneme service endpoint that accepts ZIP file uploads — for example, a batch data import feature. An attacker constructs a ZIP file that is only a few kilobytes on disk but declares an uncompressed entry size of 2 GB in its Central Directory. When the Node.js process calls adm-zip to read the archive:
adm-zipreads the Central Directory and encounters the 2 GB size claim.- It calls
Buffer.alloc(2_000_000_000)(or equivalent) to prepare the output buffer. - The Node.js process attempts to allocate 2 GB of RAM.
- The process either crashes with an out-of-memory error or the host system begins swapping, degrading all other services.
- The attacker repeats this with a handful of concurrent requests to guarantee a full outage.
This attack requires no authentication if the upload endpoint is public, and only minimal authentication bypass if it is protected — making it a realistic threat for any web service that processes user-supplied ZIP files.
Real-World Impact for dsh-mneme
The dsh-mneme component's package-lock.json locked adm-zip at version 0.5.18. Any code path in dsh-mneme that calls adm-zip APIs such as readFile(), getEntries(), or extractAllTo() on untrusted input was potentially reachable by this attack. While the PR assessment notes the path was "not confirmed reachable," the presence of a vulnerable version in the dependency tree is sufficient risk to warrant immediate remediation — especially for a High-severity CVE.
The Fix
What Changed
The fix upgrades adm-zip from 0.5.18 to 0.6.0 in two files:
dsh-mneme/package.json— updates the declared version rangedsh-mneme/package-lock.json— pins the resolved version and updates the integrity hash
The package-lock.json diff also removes a series of "libc" constraint fields from optional platform-specific binary dependencies (entries for architectures like arm, arm64, ppc64, riscv64, s390x, x64 under both glibc and musl variants). This is a metadata cleanup that accompanied the version bump in the lock file regeneration — these "libc" fields were removed because the updated lock file format no longer requires them for platform resolution.
Before and After
Before (dsh-mneme/package-lock.json — adm-zip 0.5.18):
"adm-zip": {
"version": "0.5.18",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.18.tgz",
...
}
After (dsh-mneme/package-lock.json — adm-zip 0.6.0):
"adm-zip": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz",
...
}
How Version 0.6.0 Fixes the Problem
adm-zip 0.6.0 introduces validation of ZIP entry metadata fields before memory allocation. Specifically, it checks that declared entry sizes are consistent with the actual archive file size and enforces limits that prevent absurdly large buffer allocations from crafted headers. This means a ZIP file claiming a 2 GB uncompressed entry will be rejected early in parsing — before any large allocation occurs — rather than causing the process to run out of memory.
The fix is entirely backward-compatible for legitimate ZIP files: valid archives with accurate metadata are processed identically. Only malformed or malicious archives with inflated size fields are now rejected.
The libc Field Removals
The diff shows removal of "libc": ["glibc"] and "libc": ["musl"] entries from multiple optional platform binary entries, for example:
- "libc": [
- "glibc"
- ],
"license": "LGPL-3.0-or-later",
"optional": true,
This is a lock file normalization change introduced when regenerating package-lock.json with npm's updated resolution algorithm. These fields were used in some npm versions to further constrain which optional binary to install based on the system's C library. Their removal does not affect security — it reflects a cleaner lock file format that relies on the os and cpu fields alone for platform selection.
Prevention & Best Practices
1. Pin and Audit Your Lock Files
The vulnerability was detected because package-lock.json explicitly pinned adm-zip to 0.5.18. Lock files are your first line of defense — they make it possible for scanners like Trivy to identify exactly which vulnerable version is in use.
# Audit your npm project for known vulnerabilities
npm audit
# Use Trivy to scan your lock file directly
trivy fs --scanners vuln dsh-mneme/package-lock.json
2. Validate ZIP Input at the Application Layer
Even with a patched library, defense in depth means validating archives before passing them to any parser:
const MAX_ZIP_SIZE_BYTES = 50 * 1024 * 1024; // 50 MB limit
const MAX_ZIP_ENTRIES = 1000;
function safeExtract(zipBuffer) {
if (zipBuffer.length > MAX_ZIP_SIZE_BYTES) {
throw new Error('ZIP file exceeds maximum allowed size');
}
const zip = new AdmZip(zipBuffer);
const entries = zip.getEntries();
if (entries.length > MAX_ZIP_ENTRIES) {
throw new Error('ZIP file contains too many entries');
}
// Validate each entry's claimed uncompressed size
for (const entry of entries) {
if (entry.header.size > MAX_ZIP_SIZE_BYTES) {
throw new Error(`Entry ${entry.entryName} claims excessive uncompressed size`);
}
}
return entries;
}
3. Set Node.js Memory Limits
Use Node.js's --max-old-space-size flag to cap the heap, and run services in containers with memory limits. This won't prevent the DoS entirely but limits blast radius:
node --max-old-space-size=512 server.js
4. Enable Automated Dependency Scanning
Integrate vulnerability scanning into your CI/CD pipeline so that vulnerable dependencies are caught before they reach production:
# Example GitHub Actions step
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
severity: 'HIGH,CRITICAL'
5. Security Standards Reference
- OWASP A06:2021 – Vulnerable and Outdated Components: This vulnerability is a textbook example of the risk of using unpatched third-party libraries.
- CWE-400: Uncontrolled Resource Consumption — the root cause category for this class of ZIP bomb / memory exhaustion attack.
- CWE-770: Allocation of Resources Without Limits or Throttling — the specific allocation pattern exploited here.
Key Takeaways
adm-zipversions before0.6.0trust ZIP Central Directory size fields without bounds checking — a single crafted upload can exhaust Node.js heap memory.- The
dsh-mneme/package-lock.jsonlock file pinned the vulnerable0.5.18version, which is exactly why lock file scanning with tools like Trivy is essential — it catches the precise version in use. - Upgrading to
adm-zip 0.6.0is the complete fix — no application code changes are needed; the library itself now validates metadata before allocation. - ZIP-based DoS attacks require no decompression — the damage happens at the header-reading stage, making even "read-only" archive inspection code vulnerable.
- Defense in depth matters: combine library patching with application-layer size limits and container memory caps to minimize the impact of any future similar vulnerabilities.
How Orbis AppSec Detected This
- Source: A ZIP file supplied via user-controlled input to any
dsh-mnemecode path that invokesadm-zipAPIs (e.g.,new AdmZip(userSuppliedBuffer)orreadFile(userPath)). - Sink:
adm-zip's internal buffer allocation during ZIP Central Directory parsing — specifically, theBuffer.alloc()call sized from an unvalidatedentry.header.sizefield inadm-zip 0.5.18. - Missing control: No bounds validation on ZIP entry size metadata fields before memory allocation; no maximum allocation limit enforced by the library.
- CWE: CWE-400 – Uncontrolled Resource Consumption.
- Fix: Upgraded
adm-zipfrom0.5.18to0.6.0indsh-mneme/package-lock.jsonanddsh-mneme/package.json, which introduces metadata validation before buffer allocation.
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 the attack surface of a Node.js application extends well beyond the code your team writes. A single unpatched npm dependency — in this case adm-zip 0.5.18 in dsh-mneme — can expose your entire service to a high-severity Denial of Service attack that requires nothing more than a malformed ZIP file. The fix is straightforward: upgrade to adm-zip 0.6.0. But the broader lesson is to treat your package-lock.json as a security artifact, scan it continuously, and apply patches promptly when vulnerabilities in dependencies are disclosed.