From 0151b3e23e8d897675b1e5392e55565d71f91316 Mon Sep 17 00:00:00 2001 From: yhirose Date: Tue, 18 Aug 2026 22:29:26 -0400 Subject: [PATCH] Match the Upgrade websocket token rather than the whole field value RFC 9110 7.8 defines Upgrade as a comma-separated list of protocols and asks recipients to match each protocol-name case-insensitively; RFC 6455 4.2.1 asks for a header field containing the value "websocket". Both handshake checks instead read occurrence zero and required the whole field value to be exactly "websocket", so a client offering "websocket, HTTP/3.0" -- or naming websocket on a second Upgrade field line -- was answered 404 rather than 101. This is the defect ffe2a1c fixed for Connection two lines below, and has_header_token() is already called in both of these functions. The client-side check loosens what we accept back from a server, which is the same reading: a server answering 101 may name websocket alongside another protocol, and rejecting that handshake was ours to get wrong. --- httplib.h | 16 +++++++--------- test/test.cc | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 9 deletions(-) diff --git a/httplib.h b/httplib.h index d37b9a8e..b666a6a2 100644 --- a/httplib.h +++ b/httplib.h @@ -5296,11 +5296,11 @@ inline std::string websocket_accept_key(const std::string &client_key) { inline bool is_websocket_upgrade(const Request &req) { if (req.method != "GET") { return false; } - // Check Upgrade: websocket (case-insensitive) - auto upgrade_it = req.headers.find("Upgrade"); - if (upgrade_it == req.headers.end()) { return false; } - auto upgrade_val = case_ignore::to_lower(upgrade_it->second); - if (upgrade_val != "websocket") { return false; } + // Check Upgrade: websocket. RFC 9110 7.8 defines Upgrade as a comma-separated + // list of protocols and asks recipients to match each name + // case-insensitively, so look for the token rather than compare the whole + // field value. + if (!has_header_token(req.headers, "Upgrade", "websocket")) { return false; } // Check Connection: Upgrade if (!has_header_token(req.headers, "Connection", "upgrade")) { return false; } @@ -7912,10 +7912,8 @@ inline bool read_websocket_upgrade_response(Stream &strm, return false; } - // Verify Upgrade: websocket (case-insensitive) - auto upgrade_it = headers.find("Upgrade"); - if (upgrade_it == headers.end() || - case_ignore::to_lower(upgrade_it->second) != "websocket") { + // Verify Upgrade: websocket (a comma-separated list, matched per token) + if (!has_header_token(headers, "Upgrade", "websocket")) { upgrade.error = Error::WebSocketHandshake; return false; } diff --git a/test/test.cc b/test/test.cc index 8640de3e..d3c9c28e 100644 --- a/test/test.cc +++ b/test/test.cc @@ -21561,6 +21561,43 @@ TEST(WebSocketTest, ConnectionHeaderNeedsCompleteUpgradeToken) { EXPECT_FALSE(detail::is_websocket_upgrade(make_request({}))); } +TEST(WebSocketTest, UpgradeHeaderNeedsCompleteWebsocketToken) { + // RFC 9110 7.8 defines Upgrade as a comma-separated list of protocols and + // asks recipients to match each protocol-name case-insensitively, and RFC + // 6455 4.2.1 asks for a header field containing the value "websocket". A + // client naming websocket alongside another protocol, or on a second field + // line, is offering websocket; a value that merely contains "websocket" as a + // substring is a different protocol name and is not. + auto make_request = [](const std::vector &upgrade_values) { + Request req; + req.method = "GET"; + for (const auto &value : upgrade_values) { + req.headers.emplace("Upgrade", value); + } + req.headers.emplace("Connection", "Upgrade"); + req.headers.emplace("Sec-WebSocket-Key", "dGhlIHNhbXBsZSBub25jZQ=="); + req.headers.emplace("Sec-WebSocket-Version", "13"); + return req; + }; + + EXPECT_TRUE(detail::is_websocket_upgrade(make_request({"websocket"}))); + EXPECT_TRUE(detail::is_websocket_upgrade(make_request({"WebSocket"}))); + EXPECT_TRUE( + detail::is_websocket_upgrade(make_request({"websocket, HTTP/3.0"}))); + EXPECT_TRUE( + detail::is_websocket_upgrade(make_request({"HTTP/3.0 , websocket"}))); + EXPECT_TRUE( + detail::is_websocket_upgrade(make_request({"HTTP/3.0", "websocket"}))); + + EXPECT_FALSE(detail::is_websocket_upgrade(make_request({"notwebsocket"}))); + EXPECT_FALSE(detail::is_websocket_upgrade(make_request({"websocket-2"}))); + EXPECT_FALSE(detail::is_websocket_upgrade(make_request({"xwebsocket"}))); + EXPECT_FALSE( + detail::is_websocket_upgrade(make_request({"HTTP/3.0, notwebsocket"}))); + EXPECT_FALSE(detail::is_websocket_upgrade(make_request({"h2c"}))); + EXPECT_FALSE(detail::is_websocket_upgrade(make_request({}))); +} + TEST(WebSocketTest, ServerRejectsHandshakeWithoutUpgradeToken) { Server svr; svr.WebSocket("/ws", [](const Request &, ws::WebSocket &) {});