Summary
adm-zip: Unhandled error event in async DEFLATE decompression crashes Node.js process (DoS)
Advisory details
Summary
Denial of Service in adm-zip's async decompression API allows an unauthenticated attacker to crash the entire Node.js host process by supplying a single malformed ZIP file.
Details
Affected package: adm-zip
Affected versions: at least 0.6.0 (current latest); likely all versions containing the current inflateAsync implementation in methods/inflater.js
Patched version: 0.6.1
Root Cause
methods/inflater.js:12-32 (inflateAsync) creates a zlib.createInflateRaw(option) stream and feeds it attacker-controlled compressed bytes via tmp.end(inbuf), but never registers an "error" listener on the stream:
inflateAsync: function (/*Function*/ callback) {
var tmp = zlib.createInflateRaw(option),
parts = [],
total = 0;
tmp.on("data", function (data) { parts.push(data); total += data.length; });
tmp.on("end", function () { /* build buf, callback(buf) */ });
tmp.end(inbuf); // no tmp.on("error", ...) registered anywhere
}
Per Node.js EventEmitter/stream semantics, an "error" event emitted with zero listeners is rethrown as an uncaught exception on a later tick, originating from the zlib C++ binding. This cannot be caught by a try/catch wrapped around the calling code, because the throw happens asynchronously, outside the synchronous call stack the try/catch covers.
This code path is reached from every public async API that decompresses entry data: readFileAsync, readAsTextAsync, extractAllToAsync, and ZipEntry.getDataAsync (zipEntry.js:51, :97-120, :309-315; adm-zip.js:157-164, :192-210, :893).
This library recently patched CVE-2026-39244 (GHSA-xcpc-8h2w-3j85), an unbounded Buffer.alloc() on the synchronous decompression path. That fix added a maxOutputLength option to zlib.inflateRawSync/zlib.createInflateRaw, and the sync path's resulting throw is naturally catchable. The async path shares the same maxOutputLength option (methods/inflater.js:5) but has no error-handling on the stream at all, so it was not covered by that fix and remains exploitable via either of two independent triggers:
- A DEFLATE entry with corrupted/malformed compressed bytes (
Z_DATA_ERROR) — no special crafting needed. - Compressed data whose inflated size exceeds the declared central-directory size, which now trips the
maxOutputLengthguard — but on the stream this surfaces via the unhandled"error"event rather than a catchable throw.
Attack Vector
- Attacker crafts (or corrupts) a ZIP file containing one DEFLATE-compressed entry with invalid/corrupted compressed bytes. Headers, CRC, and offsets can remain fully valid — only the compressed payload bytes need to be malformed.
- Victim application accepts this ZIP as an untrusted upload and processes it via any of adm-zip's async APIs, e.g.:
const zip = new AdmZip(uploadedBuffer); zip.readFileAsync(zip.getEntries()[0], (data) => { /* ... */ }); inflateAsyncbegins decompressing; zlib emits"error"onZ_DATA_ERROR.- No listener exists for that event, so Node rethrows it as an uncaught exception, crashing the entire host process — killing all in-flight requests for every other user/tenant on that process, not just the attacker's own request.
Impact
Any Node.js service that accepts untrusted ZIP uploads and processes them via adm-zip's async API (the documented, recommended pattern for non-blocking servers) can be crashed by a single unauthenticated request containing one small malicious file. This is a full process-level denial of service, not a per-request error.
Proof of Concept
Attached: poc_async_dos.py. Summary of what it does:
- Builds a fully valid ZIP using adm-zip's own writer (
new AdmZip(); zip.addFile(...); zip.toBuffer()), guaranteeing correct headers/CRC/offsets. - Locates the local file header's compressed-data region via its own (untouched) size/offset fields and XORs every byte in that region with
0xFF, corrupting only the DEFLATE payload. - Parses the corrupted archive with a fresh
new AdmZip(badBuf)(succeeds — headers are intact) and callsentries[0].getDataAsync(callback), wrapped intry/catch, in an isolated child process. - Captures the child's exit code and stderr.
Verified independently 3/3 runs (plus 2 isolating controls: an unmodified zip through the same path does not crash; bare Node zlib.createInflateRaw() fed garbage with no error listener reproduces the identical crash outside adm-zip entirely, confirming the root cause is the missing listener, not something else). Representative output:
[*] Child process exit code: 1
----- Node child process stderr (crash evidence) -----
node:events:497
throw er; // Unhandled 'error' event
^
Error: invalid distance too far back
at genericNodeError (node:internal/errors:983:15)
at Zlib.zlibOnError [as onerror] (node:zlib:191:17)
Emitted 'error' event on InflateRaw instance at:
at emitErrorNT (node:internal/streams/destroy:170:8)
at emitErrorCloseNT (node:internal/streams/destroy:129:3)
at process.processTicksAndRejections (node:internal/process/task_queues:89:21) {
errno: -3,
code: 'Z_DATA_ERROR'
}
Node.js v22.22.1
--------------------------------------
[*] Caught by harness's own try/catch (would mean NOT vulnerable): False
[*] getDataAsync callback ever fired (would mean NOT vulnerable): False
[*] Child process exited non-zero (crashed): True
[+] VULNERABILITY CONFIRMED
Reproduction: python3 poc_async_dos.py (requires python3 and Node.js; tested on Node.js v22.22.1).
Suggested Fix
Register an "error" listener on the InflateRaw stream in methods/inflater.js's inflateAsync, and route it to the existing callback, e.g.:
inflateAsync: function (/*Function*/ callback) {
var tmp = zlib.createInflateRaw(option),
parts = [],
total = 0;
tmp.on("data", function (data) { parts.push(data); total += data.length; });
tmp.on("error", function (err) {
// surface as a normal async error instead of crashing the process
callback(Buffer.alloc(0), err); // or however this codebase's async
// error convention is expressed
});
tmp.on("end", function () { /* existing behavior */ });
tmp.end(inbuf);
}
The exact callback/error-propagation convention should match the rest of the codebase's async error handling style (a quick look suggests callbacks here are currently success-only; this may need a small signature adjustment or an err-first convention, at the maintainer's discretion). The key fix is simply: never leave a Node.js stream without an "error" listener when it can plausibly error on attacker-controlled input.
Full PoC Source (poc_async_dos.py)
#!/usr/bin/env python3
"""
Tested version: adm-zip 0.6.0
Tested on: Linux, Node.js v22.22.1
Description:
methods/inflater.js:12-32 (inflateAsync) creates a
zlib.createInflateRaw(option) stream and calls tmp.end(inbuf) without ever
registering an "error" listener on the stream. Per Node.js EventEmitter
semantics, an "error" event emitted with zero listeners is rethrown as an
uncaught exception -- this happens on a later tick from the zlib C++
binding, so it CANNOT be caught by a try/catch wrapped around the calling
code. Any code that feeds an untrusted zip file into one of adm-zip's
public *Async APIs (readFileAsync, readAsTextAsync, extractAllToAsync,
ZipEntry.getDataAsync) crashes the entire host Node.js process the moment
it encounters a DEFLATE entry with corrupted/malformed compressed bytes.
The synchronous decompression path (getData()) was hardened for
CVE-2026-39244 (maxOutputLength + a throw that is naturally catchable);
this async streaming path was missed by that fix and remains an
unauthenticated, single-request availability bug.
Impact:
Any service that accepts untrusted zip uploads and read
References
Related vulnerabilities
All Supply chain →- MEDIUMCVE-2026-107392
music-metadata: uncatchable process crash parsing a crafted `.dsf` (residual of GHSA-v6c2-xwv6-8xf7)
- MEDIUMCVE-2026-95838
containerd: Unpack DoS via repeated opaque whiteouts
- HIGHCVE-2026-84994
Qwik: Crashing any Qwik Server via Resource thenable
- MEDIUMCVE-2026-100350
kin-openapi deepObject query decoding repeatedly compiles an invariant regular expression
- MEDIUMCVE-2026-100347
kin-openapi: Unauthenticated panic (DoS) in openapi3filter legacy router via non-canonical/unsupported HTTP method on a static path
- HIGHCVE-2026-97394
kin-openapi: Nil-pointer dereference panic in openapi3filter for OpenAPI 3.1 array schemas missing items