Back to Blog
high SEVERITY3 min read

KNX Project Extractor ZIP Bomb: Unbounded Decompression Before Size

The KNX project extractor used `@zip.js/zip.js` to decompress .knxproj files without enforcing maximum entry sizes, total archive sizes, or compression ratios. This allowed attackers to upload ZIP bombs that expanded exponentially—like the famous 42.zip producing 4.5PB from 42KB—consuming all available memory before the existing `Checked` validation could trigger. The fix introduces three hard limits: 512MB per entry, 1GB total per archive, and a 100:1 compression ratio ceiling.

O
By Orbis AppSec
•Published September 25, 2026•Reviewed September 25, 2026

Answer Summary

The `extract()` function in the KNX project processing library accepted arbitrary .knxproj archives without pre-decompression size validation. An attacker could upload a ZIP bomb with extreme compression ratios—such as 100 million to one—to exhaust server memory and CPU, causing denial of service before any size check executed. The fix adds `MAX_ENTRY_UNCOMPRESSED_SIZE`, `MAX_TOTAL_UNCOMPRESSED_SIZE`, and `MAX_COMPRESSION_RATIO` guards at the start of extraction. CWE-409 (Improper Handling of Highly Compressed Data).

Vulnerability at a Glance

cweCWE-409
fixHard limits on entry size, total size, and compression ratio before decompression begins
riskDenial of service via memory exhaustion from malicious .knxproj uploads
languageJavaScript (Node.js)
root cause`ZipReader` decompression without pre-size validation or compression ratio limits
vulnerabilityZIP bomb / decompression bomb

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:

  1. Checks compression ratio: compressedSize * MAX_COMPRESSION_RATIO < declaredUncompressedSize triggers immediate rejection
  2. Enforces per-entry limits: Individual entries exceeding 512MB abort extraction
  3. 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.js defaults 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.

Prevention and further reading

Frequently Asked Questions

Does the fix change how valid .knxproj files are processed, or only reject malicious ones?

Valid files under 512MB per entry, 1GB total, and 100:1 compression ratio process identically. Only archives exceeding these thresholds—characteristic of ZIP bombs—are rejected.

Why was the `Checked` class from `@zip.js/zip.js` insufficient protection?

`Checked` validates size *after* decompression completes. A ZIP bomb like 42.zip expands from 42KB to 4.5PB during inflation, exhausting memory before any post-check can abort the operation.

Is the 100:1 `MAX_COMPRESSION_RATIO` sufficient for legitimate KNX project files?

Yes. Typical KNX project files use modest compression; ratios above 100:1 indicate either pathological data or deliberate attack. This threshold catches 42.zip's 100-billion-to-one ratio with substantial margin.

View the Security Fix

Check out the pull request that fixed this vulnerability

View PR #760

Related Articles

high

brace-expansion DoS: Exponential Backtracking in Nested Brace Patterns

A critical vulnerability in brace-expansion allows attackers to cause denial of service by submitting specially crafted patterns with nested braces. The exponential-time complexity in pattern expansion creates a computationally expensive path that can freeze applications processing user-controlled input.

high

CVE-2026-67213: nanoid customAlphabet Infinite Loop Fix

nanoid, a widely-used ID generator pulled in transitively through postcss and vitepress, had an infinite-loop bug in its `customAlphabet` code path before version 5.1.6. This PR pins the entire dependency tree to nanoid 5.1.16 via a pnpm override so no transitive consumer can resolve back to the vulnerable 3.3.16 release.

high

Spring Boot Actuator Wildcard Exposure in 2021.04 Provisioning

A misconfigured Spring Boot Actuator in the ArkCase 2021.04 provisioning template exposed all management endpoints through wildcard inclusion. The fix narrows exposure to health and info endpoints only, eliminating unauthenticated access to sensitive runtime data.

critical

deleteNestedProperty Prototype Pollution via Dot-Notation Path

The `deleteNestedProperty` function in propertyUtils.ts allowed attackers to manipulate JavaScript object prototypes by passing specially crafted dot-notation paths like `__proto__.polluted`. A fix now blocks dangerous keys before processing, preventing prototype pollution attacks that could affect all objects in the application.

high

How Denial of Service via Infinite Loop Happens in JavaScript Dependencies and How to Fix It

CVE-2026-67213 is a high-severity denial of service vulnerability in nanoid before version 5.1.6 that triggers an infinite loop during random ID generation when processing specially crafted input. We upgraded nanoid across the entire dependency tree to patch this flaw and prevent attackers from freezing application threads. This fix ensures that ID generation remains resilient even when handling adversarial input patterns.

high

smol-toml 1.7.0 DoS: Malformed TOML Documents Crash Parser

A denial-of-service vulnerability in smol-toml 1.7.0 allows attackers to crash the parser by supplying malformed TOML documents. The vulnerability affects any application that parses untrusted TOML input. The fix, available in smol-toml 1.7.1, hardens input validation and error recovery.