no-unbounded-decompression
Require a maxOutputLength ceiling on zlib one-shot decompression
Detects zlib's buffer-at-once decompressors (gunzip, inflate, unzip, brotliDecompress, zstdDecompress and their *Sync twins) called without a maxOutputLength cap. This rule is part of eslint-plugin-node-security and provides LLM-optimized error messages.
🚨 Security rule | ⚠️ Set to error in recommended
Quick Summary
| Aspect | Details |
|---|---|
| CWE Reference | CWE-409 (Compressed Data) |
| Severity | High (denial of service) |
| Auto-Fix | ❌ Manual — only you know the right ceiling |
| Category | Security |
| ESLint MCP | ✅ Optimized for ESLint MCP integration |
| Best For | Services that accept Content-Encoding: gzip bodies or uploaded archives |
Vulnerability and Risk
Vulnerability: zlib.gunzip(body, cb) buffers the entire expansion in memory before the callback runs. There is no back-pressure and no ceiling.
Risk: DEFLATE reaches roughly 1000:1 on crafted input, and nested formats do far better. A ~1 KB request body becomes gigabytes of heap and the process dies — one request, no loop, no recursion, nothing a rate limiter would even count as abusive. maxOutputLength makes zlib abort with ERR_BUFFER_TOO_LARGE once output passes the cap, which is the only in-band defence Node offers.
Rule Details
The rule resolves the receiver to what it was imported from, not to what it is called — const z = require('node:zlib') is the same API, and a local helper named gunzip is not. It reports a call when there is no options object at all, or when the options literal carries no maxOutputLength.
It stays quiet where it would be guessing:
- the options value is opaque (
zlib.gunzip(body, opts, cb),{ ...defaults }) — the cap may already be in there; - the payload is a literal or
Buffer.from('<literal>')— a checked-in blob expands to exactly what the author put in it, so no attacker steers it.
Rule partition: the streaming factories (zlib.createGunzip, createUnzip, createInflate) are owned by secure-coding/no-unlimited-resource-allocation. This rule owns only the buffer-at-once entry points, so a given call site has exactly one owner.
Examples
❌ Incorrect
const zlib = require('zlib');
// The whole expansion lands in memory before `cb` runs
function inflateBody(reqBody, cb) {
zlib.gunzip(reqBody, (err, buf) => cb(err, buf && buf.toString('utf8')));
}
// Sync is worse: it also blocks the event loop while it does it
const plain = zlib.gunzipSync(uploadedBuffer);
// Options present, but no ceiling among them
zlib.gunzip(reqBody, { chunkSize: 4096 }, cb);✅ Correct
const zlib = require('zlib');
const MAX_OUTPUT = 10 * 1024 * 1024; // 10 MB ceiling
function inflateBody(reqBody, cb) {
zlib.gunzip(reqBody, { maxOutputLength: MAX_OUTPUT }, (err, buf) => {
if (err) return cb(err); // ERR_BUFFER_TOO_LARGE lands here
cb(null, buf.toString('utf8'));
});
}
const plain = zlib.gunzipSync(uploadedBuffer, { maxOutputLength: MAX_OUTPUT });Configuration
| Option | Type | Default | Description |
|---|---|---|---|
allowInTests | boolean | false | Allow unbounded decompression in test files |
{
rules: {
'node-security/no-unbounded-decompression': ['error', {
allowInTests: false
}]
}
}Security Impact
| Vulnerability | CWE | OWASP | CVSS | Impact |
|---|---|---|---|---|
| Decompression Bomb | 409 | A05:2021 | 7.5 High | Heap exhaustion, process kill |
| Uncontrolled Resource Use | 400 | A05:2021 | 7.5 High | Denial of service |
Related Rules
no-zip-slip— Detect archive extraction path traversalno-unsafe-buffer-alloc— Detect uninitialized buffer allocation
Known False Negatives
Promisified and opaque options
Why: A promisified decompressor is called through a wrapper this rule cannot resolve, and an options object it cannot read may already carry the cap — reporting it would be a guess.
// ❌ NOT DETECTED — the callee is a wrapper, not zlib.gunzip
const gunzipAsync = util.promisify(zlib.gunzip);
await gunzipAsync(reqBody);
// ❌ NOT DETECTED — `opts` may or may not carry maxOutputLength
zlib.gunzip(reqBody, opts, cb);Mitigation: Define one wrapper that applies the ceiling and route all decompression through it.
Further Reading
- Node zlib: class options —
maxOutputLengthsemantics - CWE-409: Improper Handling of Highly Compressed Data — Official CWE entry
- A better zip bomb — David Fifield on compression ratios
⚙️ Options
| Option | Type | Default | Description |
|---|---|---|---|
allowInTests | boolean | false | Allow unbounded decompression in test files |
Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.