adm-zip: Unhandled error event in async DEFLATE decompression crashes Node.js process (DoS)
CVE-2026-114102 · GHSA-8238-w5pm-2374 · published 29 Sept 2026
How serious is it
- Severity: high.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-114102?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
adm-zip(npm) < 0.6.1
How to check
- Run
npm ls adm-zipin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
- Or run
npx shippable-dev scan localhost:3000in your project folder. It reads your lockfile and flags this vulnerability along with every other known one.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
adm-zipto 0.6.1 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
## 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; })…
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/cthackers/adm-zip/security/advisories/GHSA-8238-w5pm-2374
- github.com/cthackers/adm-zip/commit/5e70d3a26097d68fa981c41f022c053117174e49
- github.com/cthackers/adm-zip
- github.com/cthackers/adm-zip/releases/tag/v0.6.1
Merged from OSV. Last updated 25 Oct 2026. All vulnerabilities · RSS · JSON