GitHub Advisoryadvisory · npm

GHSA-8238-w5pm-2374 adm-zip: Unhandled error event in async DEFLATE decompression crashes Node.js process (DoS)

HIGH Published 2026-09-29 Modified 2026-09-29 Official page ↗
Ecosystem
npm
Packages
adm-zip
Severity
High
CVSS vector
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
Weaknesses
CWE-248, CWE-400
Reviewed
2026-09-29

Affected packages

• npm adm-zip: < 0.6.1 → fixed in 0.6.1

Description

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: ```js 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: 1. A DEFLATE entry with corrupted/malformed compressed bytes (`Z_DATA_ERROR`) — no special crafting needed. 2. Compressed data whose inflated size exceeds the declared central-directory size, which now trips the `maxOutputLength` guard — but on the stream this surfaces via the unhandled `"error"` event rather than a catchable throw. Attack Vector 1. 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. 2. Victim application accepts this ZIP as an untrusted upload and processes it via any of adm-zip's async APIs, e.g.: ```js const zip = new AdmZip(uploadedBuffer); zip.readFileAsync(zip.getEntries()[0], (data) => { /* ... */ }); ``` 3. `inflateAsync` begins decompressing; zlib emits `"error"` on `Z_DATA_ERROR`. 4. 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: 1. Builds a fully valid ZIP using adm-zip's own writer (`new AdmZip(); zip.addFile(...); zip.toBuffer()`), guaranteeing correct headers/CRC/offsets. 2. 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. 3. Parses the corrupted archive with a fresh `new AdmZip(badBuf)` (succeeds — headers are intact) and calls `entries[0].getDataAsync(callback)`, wrapped in `try/catch`, in an isolated child process. 4. 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.: ```js 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`) ```python !/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 reads/extracts them via adm-z…

Related

Weakness · CWE (2)

  • CWE-248 Uncaught Exception weakness
  • CWE-400 Uncontrolled Resource Consumption weakness

References