Summary
Quadratic-complexity CPU exhaustion in Bandit's WebSocket fragment reassembly. Each non-final continuation frame is appended to a left-nested iolist and the entire accumulated buffer is re-measured with IO.iodata_length/1 on every frame, giving O(n^2) work in the frame count. Any unauthenticated client that can open a WebSocket can trigger it.
Details
1. Accumulation. Bandit.WebSocket.Connection.handle_frame/3 (lib/bandit/websocket/connection.ex) conses each non-final continuation frame onto the running iolist and stores it back into fragment_frame. The append is O(1).
2. Quadratic guard. Bandit.WebSocket.Connection.oversize_message?/2 then calls IO.iodata_length/1 on the whole buffer to compare against max_fragmented_message_size. Walking the buffer on every frame makes reassembly O(n^2) in the continuation-frame count.
3. No effective cap. max_fragmented_message_size (default 8 MB) bounds bytes, not frames, and 1-byte continuations are valid. Millions of tiny frames fit under the byte cap while forcing on the order of 10^13 traversal steps. The zero-byte-frame guard does not help.
4. Read timeout is idle-only. Bandit's WebSocket read timeout applies between WebSock.handle_in/2-driving callbacks; the reassembly loop runs synchronously inside a single callback, so the timeout never fires during the CPU-bound work.
PoC
- Open a WebSocket to any Bandit endpoint (Phoenix
/socket, /live, custom upgrade).
- Send an initial binary frame with
fin=0 and a 1-byte payload.
- Stream tens of thousands of 1-byte
fin=0 continuation frames, staying under max_fragmented_message_size.
- Close with a
fin=1 continuation. Server burns seconds to hours in Bandit.WebSocket.Connection.oversize_message?/2. Scale across connections to starve all schedulers.
Impact
An unauthenticated remote attacker can pin a BEAM scheduler per connection with modest bandwidth, and scale to full server DoS via concurrent connections. Any application serving WebSockets through Bandit is affected, including stock Phoenix Channels and LiveView.
References
Summary
Quadratic-complexity CPU exhaustion in Bandit's WebSocket fragment reassembly. Each non-final continuation frame is appended to a left-nested iolist and the entire accumulated buffer is re-measured with
IO.iodata_length/1on every frame, giving O(n^2) work in the frame count. Any unauthenticated client that can open a WebSocket can trigger it.Details
1. Accumulation.
Bandit.WebSocket.Connection.handle_frame/3(lib/bandit/websocket/connection.ex) conses each non-final continuation frame onto the running iolist and stores it back intofragment_frame. The append is O(1).2. Quadratic guard.
Bandit.WebSocket.Connection.oversize_message?/2then callsIO.iodata_length/1on the whole buffer to compare againstmax_fragmented_message_size. Walking the buffer on every frame makes reassembly O(n^2) in the continuation-frame count.3. No effective cap.
max_fragmented_message_size(default 8 MB) bounds bytes, not frames, and 1-byte continuations are valid. Millions of tiny frames fit under the byte cap while forcing on the order of 10^13 traversal steps. The zero-byte-frame guard does not help.4. Read timeout is idle-only. Bandit's WebSocket read timeout applies between
WebSock.handle_in/2-driving callbacks; the reassembly loop runs synchronously inside a single callback, so the timeout never fires during the CPU-bound work.PoC
/socket,/live, custom upgrade).fin=0and a 1-byte payload.fin=0continuation frames, staying undermax_fragmented_message_size.fin=1continuation. Server burns seconds to hours inBandit.WebSocket.Connection.oversize_message?/2. Scale across connections to starve all schedulers.Impact
An unauthenticated remote attacker can pin a BEAM scheduler per connection with modest bandwidth, and scale to full server DoS via concurrent connections. Any application serving WebSockets through Bandit is affected, including stock Phoenix Channels and LiveView.
References