Decompression Bombs: Stopping Zip and Gzip Uploads From Exhausting Memory and Disk

A 200 KiB gzip body can inflate to 200 MiB in a single call. See the bug reproduced in Node.js, then size-capped fixes for JavaScript, Python, Java and Go.

Applications that accept compressed input, whether a gzip request body, a zip upload or a tarball import, often trust the archive to be reasonable. A decompression bomb is a small file that expands to something huge, exhausting memory, disk or CPU. We chose this as an evergreen topic because no existing article here covers it (our zip-slip article covers path traversal, not size). We found no fresh news item as of 1 November 2026 that met our bar for developer relevance: a web search returned mostly material from April to August 2026 and nothing dated November.

What the references say

CWE-409 (opened) is named “Improper Handling of Highly Compressed Data (Data Amplification)”. It describes a product that does not handle, or incorrectly handles, compressed input with a very high compression ratio that produces a large output. The page we received had no mitigations section.

The Python zipfile documentation (opened) says decompression bombs “apply to zipfile library that can cause disk volume exhaustion”, and warns: “Never extract archives from untrusted sources without prior inspection.”

The Java ZipEntry documentation (opened) says getSize() returns the uncompressed size “or -1 if not known”. The page says nothing about whether the value is verified against the real data. Our inference, not a documented claim: treat declared sizes as attacker-controlled hints and count the bytes you actually read.

The vulnerable pattern

The bug is calling a convenience decompressor with no output ceiling. Node’s zlib documentation (opened, first 100,000 of about 175,000 characters) lists a maxOutputLength option for the convenience methods, defaulting to buffer.kMaxLength. That default is effectively “as much as fits in a Buffer”.

We ran this on Node.js v22:

import zlib from 'node:zlib';

const bomb = zlib.gzipSync(Buffer.alloc(200 * 1024 * 1024)); // 200 MiB of zeros
console.log('compressed bytes', bomb.length);

// VULNERABLE: no output limit
const out = zlib.gunzipSync(bomb);
console.log('inflated', out.length);

Output on our machine: 203,860 compressed bytes inflated to 209,715,200 bytes (about 1,000 to 1), in roughly 200 ms. One request, one synchronous call, and the event loop was busy while 200 MiB was allocated. Scale the zeros up and the same code exhausts the process heap. Zeros compress unusually well, so a real attacker’s ratio will vary by payload and format.

Fix 1: cap the output

In Node, pass a limit and handle the failure. This ran locally: the same bomb was rejected with ERR_BUFFER_TOO_LARGE, and a small valid payload still decoded. We observed that error code in our run; the documentation page we read did not state it.

const MAX = 1024 * 1024; // 1 MiB, pick a number from your real traffic

function safeGunzip(buf) {
  try {
    return zlib.gunzipSync(buf, { maxOutputLength: MAX });
  } catch (e) {
    if (e.code === 'ERR_BUFFER_TOO_LARGE') throw new Error('payload too large');
    throw e;
  }
}

In production prefer the async or streaming API so a slow inflate does not block other requests, and also cap the compressed size at the HTTP layer.

Fix 2: count bytes, never trust headers

For archives, enforce three limits: bytes per entry, total bytes, and entry count. Count what you actually read. The Python, Java and Go samples below are untested: no Python runtime or Go toolchain was available, and we did not compile the Java. They follow the documentation we opened, so run them before relying on them.

Python:

import zipfile

MAX_ENTRY = 10 * 1024 * 1024
MAX_TOTAL = 50 * 1024 * 1024
MAX_FILES = 1000

def read_zip_safely(path):
    total = 0
    with zipfile.ZipFile(path) as z:
        infos = z.infolist()
        if len(infos) > MAX_FILES:
            raise ValueError("too many entries")
        for info in infos:
            with z.open(info) as f:
                data = f.read(MAX_ENTRY + 1)  # read one extra byte to detect overflow
            if len(data) > MAX_ENTRY:
                raise ValueError("entry too large")
            total += len(data)
            if total > MAX_TOTAL:
                raise ValueError("archive too large")
            yield info.filename, data

Java, counting as you read rather than trusting getSize():

static byte[] readEntry(ZipInputStream zis, long maxBytes) throws IOException {
    ByteArrayOutputStream out = new ByteArrayOutputStream();
    byte[] buf = new byte[8192];
    long total = 0;
    int n;
    while ((n = zis.read(buf)) != -1) {
        total += n;
        if (total > maxBytes) throw new IOException("entry too large");
        out.write(buf, 0, n);
    }
    return out.toByteArray();
}

Go, using io.LimitReader, which per the Go io documentation (opened) returns EOF after n bytes. Because truncation looks like a normal end of stream, read one byte past the limit and check:

func readLimited(r io.Reader, max int64) ([]byte, error) {
	data, err := io.ReadAll(io.LimitReader(r, max+1))
	if err != nil {
		return nil, err
	}
	if int64(len(data)) > max {
		return nil, errors.New("payload too large")
	}
	return data, nil
}

The Go pitfall matters: a bare LimitReader silently truncates instead of failing, so a bomb becomes a corrupted but accepted file.

Where to look in review

  • Any call to gunzipSync, inflateSync, GZIPInputStream, gzip.NewReader, zipfile.extractall or tar extraction on user-supplied data.
  • Middleware that auto-decompresses request bodies (check its size option, if any; we did not verify per-framework defaults).
  • Nested archives, where an extractor unpacks a zip inside a zip.
  • Image, PDF and document parsers that inflate internally. We have no evidence for specific libraries from the sources we opened, so check their documented limits.

Checklist

  1. Cap compressed input size at the edge, before the app reads it.
  2. Cap decompressed bytes per stream, per entry and per archive, and cap entry count and nesting depth.
  3. Count bytes actually read; do not rely on declared sizes.
  4. Run decompression with timeouts and memory limits, ideally in a worker or sandbox.
  5. Fail closed with a clear error and log the rejection.

Sources