# Vulnerability Summary ## Vulnerability Overview This vulnerability involves the handling of fragmented frames in the WebSocket protocol. Specifically: - There is no limit on the maximum length of fragmented messages, which may lead to Denial of Service (DoS) attacks. - Zero-length non-fin continuation frames are not handled correctly, potentially causing abnormal behavior. ## Impact Scope - All server-side implementations using the WebSocket protocol, especially in scenarios where `max_fragmented_message_size` is not correctly configured. - Attackers can cause server resource exhaustion or crashes by sending oversized fragmented messages or zero-length non-fin continuation frames. ## Remediation Plan 1. **Define a maximum fragmented message size limit**: - Add a new configuration item `max_fragmented_message_size` with a default value of 8,000,000 bytes (8MB). - Enforce validation of the fragmented message size in the code; reject the connection if the limit is exceeded. 2. **Prohibit zero-length non-fin continuation frames**: - When receiving fragmented messages, check if the frame is zero-length and non-fin; if so, close the connection immediately. 3. **Update test cases**: - Add test cases targeting the above fixes to ensure the vulnerability is thoroughly remediated. ## POC Code The following are key code snippets after the fix: ### Configuration Definition ```elixir defmodule Bandit do @typedoc """ Options for the WebSocket server. """ @typedoc since: "1.0.0" @type options() :: [ enabled: boolean(), max_frame_size: pos_integer(), max_fragmented_message_size: pos_integer(), validate_text_frames: boolean(), compress: boolean(), deflate_options: Bandit.WebSocket.DeflateOptions.t() ] @doc """ Default options for the WebSocket server. """ @doc since: "1.0.0" @spec default_options() :: options() def default_options() do [ enabled: true, max_frame_size: 1_000_000, max_fragmented_message_size: 8_000_000, validate_text_frames: true, compress: false, deflate_options: Bandit.WebSocket.DeflateOptions.default() ] end end ``` ### Fragmented Message Size Validation ```elixir defp oversize_message?(data, opts) do case Keyword.get(opts, :max_fragmented_message_size, 8_000_000) do 0 -> false max_fragmented_message_size -> IO.iodata_length(data) > max_fragmented_message_size end end ``` ### Handling Zero-Length Non-Fin Continuation Frames ```elixir defp handle_frame(frame, socket, connection) do case frame do %Frame.Continuation{fin: false} = frame -> if IO.iodata_length(frame.data) == 0 do do_error(1008, "Received zero byte non-fin continuation frame", socket, connection) else data = [connection.fragment_frame_data | frame.data] if oversize_message?(data, connection.opts) do do_error(1009, "Received oversize fragmented message", socket, connection) else {:continue, %{connection | fragment_frame: %{connection.fragment_frame | data: data}}} end end _ -> # Other frame handling logic end end ``` ### Test Cases ```elixir test "zero byte fin continuation frames are accepted", context do client = SimpleWebSocketClient.tcp_client(context) SimpleWebSocketClient.http_handshake(client, TerminateWebSock) payload = String.duplicate("0123456789", 1_000) SimpleWebSocketClient.send_binary_frame(client, payload, 0x0) SimpleWebSocketClient.send_continuation_frame(client, payload, 0x0) SimpleWebSocketClient.send_continuation_frame(client, >, 0x1) expected_payload = String.duplicate(payload, 2) assert SimpleWebSocketClient.recv_binary_frame(client) == {:ok, expected_payload} end test "zero byte non-fin continuation frames are rejected", context do capture_log(fn -> client = SimpleWebSocketClient.tcp_client(context) SimpleWebSocketClient.http_handshake(client, TerminateWebSock) SimpleWebSocketClient.send_binary_frame(client, "0123456789", 0x0) SimpleWebSocketClient.send_continuation_frame(client, >, 0x0) # Retrieve the termination frame error to ensure we closed with the expected reason assert_receive {:error, "Received zero byte non-fin continuation frame"}, 500 assert SimpleWebSocketClient.recv_connection_close_frame(client) == {:ok, >} # Verify that the server did not send any additional frames assert SimpleWebSocketClient.connection_closed_for_reading?(client) Process.sleep(500) end) assert output =~ "Received zero byte non-fin continuation frame" end ```