Introduction
A ZIP bomb reached production in the KNX project extraction path, where extract() processed .knxproj archives through @zip.js/zip.js without guarding against exponential decompression. The ZipReader class dutifully inflated whatever data it received, and only afterward did the Checked wrapper validate sizes—far too late to prevent memory exhaustion from a crafted payload.
This is CWE-409: Improper Handling of Highly Compressed Data. The vulnerability sits in the gap between "start decompression" and "verify the result isn't absurdly large." That gap, measured in CPU cycles and heap allocations, is where ZIP bombs detonate.
Affected Versions
| Affected | not applicable (first-party code) |
| Fixed in | not applicable (first-party code) |
| Ecosystem | npm |
| CVE / GHSA | not assigned |
| CWE | CWE-409 (Improper Handling of Highly Compressed Data) |
The vulnerable code processed .knxproj files in Node.js environments using @zip.js/zip.js version 2.x without additional guards.
The Vulnerability Explained
The extraction routine configured ZipReader with Checked output validation:
const { BlobReader, ZipReader, Uint8ArrayWriter, configure } = require("@zip.js/zip.js");
// Disable web workers — we run in Node.js
configure({ useWebWorkers: false });
The Checked class wraps writers to enforce size limits after writing completes. But decompression happens inside ZipReader.getData()—memory allocates, CPU burns, and the event loop stalls before Checked ever sees the inflated bytes.
An attacker uploads evil.knxproj: a 10KB file containing nested ZIP archives with identical files, each layer referencing the next. The outermost file reports modest compressed sizes. When extract() calls getData(), the library recursively inflates through 16 layers, producing gigabytes of identical null bytes. The Node.js process heap grows until the OS OOM killer intervenes—or slower peers timeout, whichever comes first.
The famous 42.zip achieves 4.5 petabytes from 42 kilobytes. Against unguarded code, that's not a theoretical concern; it's a single HTTP POST away.
The Fix
The patch introduces three hard limits before any decompression begins:
// Limits to guard against ZIP bomb attacks (CWE-409)
const MAX_ENTRY_UNCOMPRESSED_SIZE = 512 * 1024 * 1024; // 512 MB per entry
const MAX_TOTAL_UNCOMPRESSED_SIZE = 1024 * 1024 * 1024; // 1 GB total per archive
const MAX_COMPRESSION_RATIO = 100; // reject entries that inflate more than 100x
These constants enable pre-flight validation. Before ZipReader inflates an entry, the code now:
- Checks compression ratio:
compressedSize * MAX_COMPRESSION_RATIO < declaredUncompressedSizetriggers immediate rejection - Enforces per-entry limits: Individual entries exceeding 512MB abort extraction
- Tracks aggregate size: Running total across all entries cannot exceed 1GB
The 100:1 ratio catches 42.zip's 100-billion-to-one attack with margin to spare, while legitimate KNX projects—typically tens of megabytes with modest compression—pass untouched.
Key Takeaways
- Post-decompression validation is too late: Size checks must precede
getData()or equivalent calls; memory pressure manifests during inflation, not after - Compression ratios reveal attacks: Legitimate archives rarely exceed 10:1; thresholds at 100:1 catch bombs without false positives
- Aggregate limits matter: ZIP bombs nest—per-entry caps alone won't stop 10,000 50MB entries from exhausting resources
@zip.js/zip.jsdefaults are permissive: The library trusts input; security boundaries must be enforced by calling code- KNX project files are attack surface: Industrial automation formats processed server-side warrant the same scrutiny as user-uploaded images
How Orbis AppSec Detected This
Source: The buffer parameter passed to extract() from HTTP request bodies containing .knxproj uploads
Sink: ZipReader.getData() invoked without pre-decompression size validation or compression ratio checks
Missing control: No enforcement of MAX_ENTRY_UNCOMPRESSED_SIZE, MAX_TOTAL_UNCOMPRESSED_SIZE, or MAX_COMPRESSION_RATIO before decompression begins; reliance on Checked class providing only post-hoc validation
CWE: CWE-409 — Improper Handling of Highly Compressed Data
Fix: Added three hard limits (512MB per entry, 1GB total, 100:1 ratio) validated before ZipReader processes archive contents
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
ZIP bombs exploit the fundamental asymmetry between compression and decompression: cheap to create, expensive to process. The KNX project extractor learned this lesson the hard way—Checked validation arrived after the damage was done. The fix demonstrates proper defense: assume malicious input, validate before expensive operations, and never trust compression ratios at face value. For any service accepting archives, these three limits—per-entry, total, and ratio—are the minimum viable security boundary.