# 漏洞总结 ## 漏洞概述 该漏洞涉及 WebSocket 协议中分帧消息(fragmented frames)的处理问题。具体表现为: - 未对分帧消息的最大长度进行限制,可能导致拒绝服务(DoS)攻击。 - 未正确处理零长度的非终止帧(non-fin continuation frames),可能引发异常行为。 ## 影响范围 - 所有使用 WebSocket 协议的服务端实现,尤其是未正确配置 `max_fragmented_message_size` 的场景。 - 攻击者可以通过发送超大分帧消息或零长度非终止帧,导致服务端资源耗尽或崩溃。 ## 修复方案 1. **定义最大分帧消息大小限制**: - 新增配置项 `max_fragmented_message_size`,默认值为 8,000,000 字节(8MB)。 - 在代码中强制校验分帧消息的大小,超出限制时拒绝连接。 2. **禁止零长度非终止帧**: - 在接收分帧消息时,检查帧是否为零长度且非终止帧,若是则直接关闭连接。 3. **更新测试用例**: - 添加针对上述修复的测试用例,确保漏洞被彻底修复。 ## POC 代码 以下是修复后的关键代码片段: ### 配置项定义 ```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 ``` ### 分帧消息大小校验 ```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 ``` ### 零长度非终止帧处理 ```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 _ -> # 其他帧处理逻辑 end end ``` ### 测试用例 ```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) # 获取终止帧的错误,确保我们关闭了预期的原因 assert_receive {:error, "Received zero byte non-fin continuation frame"}, 500 assert SimpleWebSocketClient.recv_connection_close_frame(client) == {:ok, >} # 验证服务器没有发送任何额外的帧 assert SimpleWebSocketClient.connection_closed_for_reading?(client) Process.sleep(500) end) assert output =~ "Received zero byte non-fin continuation frame" end ```