From 8fa1448c352a095613468b3eb1fa62aa39f495bc Mon Sep 17 00:00:00 2001 From: zhouyunding Date: Fri, 6 Nov 2026 08:04:18 +0800 Subject: [PATCH] Validate WebSocket frame headers --- http/WebSocketParser.cpp | 13 ++++ http/WebSocketParser.h | 1 + http/server/HttpHandler.cpp | 1 + http/websocket_parser.c | 4 ++ unittest/websocket_parser_test.cpp | 96 +++++++++++++++++++++++++++++- 5 files changed, 113 insertions(+), 2 deletions(-) diff --git a/http/WebSocketParser.cpp b/http/WebSocketParser.cpp index bdb3655c9..06b0dbc4c 100644 --- a/http/WebSocketParser.cpp +++ b/http/WebSocketParser.cpp @@ -16,6 +16,18 @@ static int on_frame_header(websocket_parser* parser) { WebSocketParser* wp = (WebSocketParser*)parser->data; int opcode = parser->flags & WS_OP_MASK; // printf("on_frame_header opcode=%d\n", opcode); + switch (opcode) { + case WS_OP_CONTINUE: + case WS_OP_TEXT: + case WS_OP_BINARY: + case WS_OP_CLOSE: + case WS_OP_PING: + case WS_OP_PONG: break; + default: return -1; + } + if (wp->is_server && !(parser->flags & WS_HAS_MASK)) { + return -1; + } if (is_control_frame(parser)) { // RFC 6455 5.5: control frames MUST NOT be fragmented and MUST have // a payload length of 125 bytes or less. @@ -88,6 +100,7 @@ WebSocketParser::WebSocketParser() { parser->data = this; state = WS_FRAME_BEGIN; opcode = WS_OP_CLOSE; + is_server = false; } WebSocketParser::~WebSocketParser() { diff --git a/http/WebSocketParser.h b/http/WebSocketParser.h index f1bd6d3e1..5ecef3481 100644 --- a/http/WebSocketParser.h +++ b/http/WebSocketParser.h @@ -21,6 +21,7 @@ class HV_EXPORT WebSocketParser { websocket_parser* parser; websocket_parser_state state; int opcode; + bool is_server; // Require masked incoming frames; false by default. std::string message; std::string control_message; std::function onMessage; diff --git a/http/server/HttpHandler.cpp b/http/server/HttpHandler.cpp index d93bbb15a..35ae5e396 100644 --- a/http/server/HttpHandler.cpp +++ b/http/server/HttpHandler.cpp @@ -206,6 +206,7 @@ bool HttpHandler::SwitchWebSocket() { protocol = WEBSOCKET; ws_parser = std::make_shared(); + ws_parser->is_server = true; ws_channel = std::make_shared(io, WS_SERVER); ws_parser->onMessage = [this](int opcode, const std::string& msg){ ws_channel->opcode = (enum ws_opcode)opcode; diff --git a/http/websocket_parser.c b/http/websocket_parser.c index 48b89dd2d..f8b1de2ac 100644 --- a/http/websocket_parser.c +++ b/http/websocket_parser.c @@ -58,6 +58,10 @@ size_t websocket_parser_execute(websocket_parser *parser, const websocket_parser for(p = data; p != end; p++) { switch(parser->state) { case s_start: + // No extensions are negotiated, so all RSV bits must be zero. + if(CC & 0x70) { + return (size_t)(p - data); + } parser->offset = 0; parser->length = 0; parser->mask_offset = 0; diff --git a/unittest/websocket_parser_test.cpp b/unittest/websocket_parser_test.cpp index 5cdfe4a38..97d6360be 100644 --- a/unittest/websocket_parser_test.cpp +++ b/unittest/websocket_parser_test.cpp @@ -1,4 +1,9 @@ +// Keep assertions active in Release builds: the checks also feed the parser. +#ifdef NDEBUG +#undef NDEBUG +#endif #include +#include #include #include @@ -17,8 +22,10 @@ static std::string frame(enum ws_opcode opcode, const std::string& payload, bool return buf; } -static size_t feed(const std::string& data, Messages* messages, bool byte_by_byte = false) { +static size_t feed(const std::string& data, Messages* messages, bool byte_by_byte = false, bool is_server = false) { WebSocketParser parser; + assert(!parser.is_server); + parser.is_server = is_server; parser.onMessage = [messages](int opcode, const std::string& msg) { messages->push_back(Message(opcode, msg)); }; @@ -95,12 +102,97 @@ static void test_reject_oversized_control_frame() { assert(messages.empty()); } -int main() { +static void test_reject_reserved_bits() { + for (int rsv = 0x10; rsv <= 0x70; rsv += 0x10) { + for (int server = 0; server < 2; ++server) { + for (int split = 0; split < 2; ++split) { + std::string data = frame(WS_OPCODE_TEXT, "hi"); + data[0] |= (char)rsv; + Messages messages; + assert(feed(data, &messages, split != 0, server != 0) < data.size()); + assert(messages.empty()); + } + } + } +} + +static void test_reject_reserved_opcodes() { + for (int opcode = 0; opcode <= 0xF; ++opcode) { + if (opcode <= 0x2 || (opcode >= 0x8 && opcode <= 0xA)) continue; + for (int server = 0; server < 2; ++server) { + for (int split = 0; split < 2; ++split) { + std::string data = frame(WS_OPCODE_TEXT, "zz"); + data[0] = (char)(0x80 | opcode); + Messages messages; + assert(feed(data, &messages, split != 0, server != 0) < data.size()); + assert(messages.empty()); + } + } + } +} + +static void test_server_mask_direction() { + const enum ws_opcode opcodes[] = {WS_OPCODE_TEXT, WS_OPCODE_BINARY, WS_OPCODE_CLOSE, WS_OPCODE_PING, WS_OPCODE_PONG}; + for (size_t i = 0; i < sizeof(opcodes) / sizeof(opcodes[0]); ++i) { + for (int empty = 0; empty < 2; ++empty) { + for (int split = 0; split < 2; ++split) { + std::string data = frame(opcodes[i], empty ? "" : "hi", true, false); + Messages messages; + assert(feed(data, &messages, split != 0, true) < data.size()); + assert(messages.empty()); + } + } + } +} + +static void test_valid_mask_direction() { + for (int server = 0; server < 2; ++server) { + for (int masked = 0; masked < 2; ++masked) { + if (server && !masked) continue; + for (int split = 0; split < 2; ++split) { + std::string data = frame(WS_OPCODE_TEXT, "Hel", false, masked != 0) + frame(WS_OPCODE_PING, "", true, masked != 0) + + frame(WS_OPCODE_CONTINUE, "lo", true, masked != 0); + Messages messages; + assert(feed(data, &messages, split != 0, server != 0) == data.size()); + assert(messages.size() == 2); + assert(messages[0] == Message(WS_OPCODE_PING, "")); + assert(messages[1] == Message(WS_OPCODE_TEXT, "Hello")); + } + } + } +} + +int main(int argc, char** argv) { + if (argc == 2) { + std::string name = argv[1]; + if (name == "rsv") { + test_reject_reserved_bits(); + } + else if (name == "opcode") { + test_reject_reserved_opcodes(); + } + else if (name == "mask") { + test_server_mask_direction(); + } + else if (name == "valid") { + test_valid_mask_direction(); + } + else { + return 1; + } + printf("PASS: %s\n", argv[1]); + return 0; + } test_ping_between_fragments(); test_control_frames_between_fragments_byte_by_byte(); test_control_frame_between_messages(); test_max_control_payload(); test_reject_fragmented_control_frame(); test_reject_oversized_control_frame(); + test_reject_reserved_bits(); + test_reject_reserved_opcodes(); + test_server_mask_direction(); + test_valid_mask_direction(); + printf("PASS: 10 WebSocket parser tests\n"); return 0; }