Improper Handling of Highly Compressed Data (Data Amplification) vulnerability in wojtekmach Req allows attacker-controlled HTTP servers to…
EEF·CWE-409·Published 2026-06-08
Improper Handling of Highly Compressed Data (Data Amplification) vulnerability in wojtekmach Req allows attacker-controlled HTTP servers to exhaust memory in a Req client via decompression-bomb response bodies. Req's default response pipeline includes Req.Steps.decode_body/1 and Req.Steps.decompress_body/1 in lib/req/steps.ex. decode_body/1 dispatches on the server-supplied content-type (or URL extension) and calls :zip.extract(body, [:memory]) for application/zip, :erl_tar.extract({:binary, body}, [:memory]) for application/x-tar, and :erl_tar.extract({:binary, body}, [:memory, :compressed]) for application/gzip / .tgz. Each returns the full decompressed archive contents as a [{name, bytes}] list in memory, with no per-entry or total size cap. decompress_body/1 walks the content-encoding header and chains :zlib/:brotli/:ezstd decoders, so a response advertising content-encoding: gzip, gzip, gzip inflates through multiple layers without bound. Both steps are enabled by default, no caller opt-in is required, and the attacker controls the content-type and content-encoding headers on their own server (or on any host reached via Req's automatic redirect following). A sub-megabyte response can expand to multiple gigabytes on the victim, crashing the BEAM process. This issue affects req: from 0.1.0 before 0.6.1.
Improper Handling of Highly Compressed Data (Data Amplification) vulnerability in wojtekmach Req allows attacker-controlled HTTP servers to exhaust memory in a Req client via decompression-bomb response bodies. Req's default response pipeline includes Req.Steps.decode_body/1 and Req.Steps.decompress_body/1 in lib/req/steps.ex. decode_body/1 dispatches on the server-supplied content-type (or URL extension) and calls :zip.extract(body, [:memory]) for application/zip, :erl_tar.extract({:binary, body}, [:memory]) for application/x-tar, and :erl_tar.extract({:binary, body}, [:memory, :compressed]) for application/gzip / .tgz. Each returns the full decompressed archive contents as a [{name, bytes}] list in memory, with no per-entry or total size cap. decompress_body/1 walks the content-encoding header and chains :zlib/:brotli/:ezstd decoders, so a response advertising content-encoding: gzip, gzip, gzip inflates through multiple layers without bound. Both steps are enabled by default, no caller opt-in is required, and the attacker controls the content-type and content-encoding headers on their own server (or on any host reached via Req's automatic redirect following). A sub-megabyte response can expand to multiple gigabytes on the victim, crashing the BEAM process. This issue affects req: from 0.1.0 before 0.6.1.
### Summary Req's default response pipeline auto-decodes archive and compressed bodies based on the server-supplied `content-type` (or URL extension) and materialises the full decompressed contents in memory with no size cap. An attacker who controls (or can redirect a victim into) an HTTP endpoint reached by `Req.get!/1` can return a tiny "decompression bomb" that expands to many gigabytes on the client and exhausts the BEAM's memory. ### Details **1. Archive auto-decoding.** `Req.Steps.decode_body/1` in `lib/req/steps.ex` dispatches on the response `content-type` (or URL extension) and calls Erlang's archive libraries with `:memory`, returning a `[{name, bytes}]` list of every entry fully decompressed in RAM: `application/zip` → `:zip.extract(body, [:memory])`, `application/x-tar` → `:erl_tar.extract({:binary, body}, [:memory])`, `application/gzip` / `.tgz` → `:erl_tar.extract({:binary, body}, [:memory, :compressed])`. No byte cap is enforced before decoding and no per-entry size limit is passed to `:zip` / `:erl_tar`. **2. content-encoding chaining.** `Req.Steps.decompress_body/1` walks the `content-encoding` header and chains `:zlib` / `:brotli` / `:ezstd` decoders, so a response advertising `content-encoding: gzip, gzip, gzip, …` inflates through multiple layers without bound. **3. Default-on, attacker-chosen decoder.** Both steps are part of Req's default pipeline. The caller does not need to opt in, and the attacker chooses which decoder fires by setting `content-type` and `content-encoding` on their own server (or on any host reached via Req's automatic redirect following). ### PoC 1. Run an HTTP server that responds 200 with `content-type: application/zip` and a body that is a zip archive whose single entry is ~400 MB of zero bytes (compressed wire payload: a few hundred KB). 2. From the victim process, call `Req.get!(url)` against that server (no special options, no opt-in to archive decoding). 3. `decode_body/1` dispatches on `content-type`, invokes `:zip.extract(body, [:memory])`, and the response body becomes `[{~c"bomb.bin", <<400 MB of zero bytes>>}]`. A sub-MB request produces hundreds of MB resident memory; layering gzip on the `content-encoding` path or increasing entry size scales arbitrarily. ### Impact Memory-exhaustion denial of service against any Elixir application that uses Req with its default step pipeline to fetch URLs influenced by an untrusted party, including webhook senders, link previews, OAuth/OIDC discovery clients, package mirrors, image proxies, and any `Req.get!/1` call that may follow redirects to attacker-controlled hosts. No authentication is required; a single response can crash the BEAM and take down unrelated workloads on the same VM.
### Summary Req's default response pipeline auto-decodes archive and compressed bodies based on the server-supplied `content-type` (or URL extension) and materialises the full decompressed contents in memory with no size cap. An attacker who controls (or can redirect a victim into) an HTTP endpoint reached by `Req.get!/1` can return a tiny "decompression bomb" that expands to many gigabytes on the client and exhausts the BEAM's memory. ### Details **1. Archive auto-decoding.** `Req.Steps.decode_body/1` in `lib/req/steps.ex` dispatches on the response `content-type` (or URL extension) and calls Erlang's archive libraries with `:memory`, returning a `[{name, bytes}]` list of every entry fully decompressed in RAM: `application/zip` → `:zip.extract(body, [:memory])`, `application/x-tar` → `:erl_tar.extract({:binary, body}, [:memory])`, `application/gzip` / `.tgz` → `:erl_tar.extract({:binary, body}, [:memory, :compressed])`. No byte cap is enforced before decoding and no per-entry size limit is passed to `:zip` / `:erl_tar`. **2. content-encoding chaining.** `Req.Steps.decompress_body/1` walks the `content-encoding` header and chains `:zlib` / `:brotli` / `:ezstd` decoders, so a response advertising `content-encoding: gzip, gzip, gzip, …` inflates through multiple layers without bound. **3. Default-on, attacker-chosen decoder.** Both steps are part of Req's default pipeline. The caller does not need to opt in, and the attacker chooses which decoder fires by setting `content-type` and `content-encoding` on their own server (or on any host reached via Req's automatic redirect following). ### PoC 1. Run an HTTP server that responds 200 with `content-type: application/zip` and a body that is a zip archive whose single entry is ~400 MB of zero bytes (compressed wire payload: a few hundred KB). 2. From the victim process, call `Req.get!(url)` against that server (no special options, no opt-in to archive decoding). 3. `decode_body/1` dispatches on `content-type`, invokes `:zip.extract(body, [:memory])`, and the response body becomes `[{~c"bomb.bin", <<400 MB of zero bytes>>}]`. A sub-MB request produces hundreds of MB resident memory; layering gzip on the `content-encoding` path or increasing entry size scales arbitrarily. ### Impact Memory-exhaustion denial of service against any Elixir application that uses Req with its default step pipeline to fetch URLs influenced by an untrusted party, including webhook senders, link previews, OAuth/OIDC discovery clients, package mirrors, image proxies, and any `Req.get!/1` call that may follow redirects to attacker-controlled hosts. No authentication is required; a single response can crash the BEAM and take down unrelated workloads on the same VM.
La vulnerabilidad de manejo inadecuado de datos altamente comprimidos (amplificación de datos) en wojtekmach Req permite a los servidores HTTP controlados por el atacante agotar la memoria en un cliente Req mediante cuerpos de respuesta de bomba de descompresión. La pipeline de respuesta predeterminada de Req incluye Req.Steps.decode_body/1 y Req.Steps.decompress_body/1 en lib/req/steps.ex. decode_body/1 despacha según el content-type proporcionado por el servidor (o extensión de URL) y llama a :zip.extract(body, [:memory]) para application/zip, a :erl_tar.extract({:binary, body}, [:memory]) para application/x-tar, y a :erl_tar.extract({:binary, body}, [:memory, :compressed]) para application/gzip / .tgz. Cada uno devuelve el contenido completo del archivo descomprimido como una lista [{name, bytes}] en memoria, sin límite de tamaño por entrada o total. decompress_body/1 recorre el encabezado content-encoding y encadena decodificadores :zlib/:brotli/:ezstd, por lo que una respuesta que anuncia content-encoding: gzip, gzip, gzip se infla a través de múltiples capas sin límite. Ambos pasos están habilitados por defecto, no se requiere la participación del llamador, y el atacante controla los encabezados content-type y content-encoding en su propio servidor (o en cualquier host alcanzado a través del seguimiento automático de redirecciones de Req). Una respuesta de menos de un megabyte puede expandirse a múltiples gigabytes en la víctima, colapsando el proceso BEAM. Este problema afecta a req: desde 0.1.0 antes de 0.6.1.
| Version | Type | Source | Base | Exp | Impact | Vector |
|---|---|---|---|---|---|---|
| 4.0 | Primary | cve.org | 8.2 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N |
| 4.0 | Secondary | NVD | 8.2 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X |
| 4.0 | Secondary | GHSA | 8.2 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N |
| 4.0 | Secondary | ENISA EUVD | 8.2 | — | — | CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N |