This is an automated email from the ASF dual-hosted git repository. markt-asf pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 95d1ba6ad81c1dda03afc224afed4843205d22f5 Author: Arturo Bernal <[email protected]> AuthorDate: Thu Nov 5 18:45:02 2026 +0100 Reject WebSocket handshakes with repeated or invalid version and key (#1113) RFC 6455, sections 11.3.1 and 11.3.5 do not allow the Sec-WebSocket-Key and Sec-WebSocket-Version header fields to appear more than once in the opening handshake request, and section 4.2.1 requires the version to be 13. A request with repeated fields, or with a version list such as "8, 13", was accepted. Reject repeated fields with a 400 response and a version other than 13 with a 426 response. --- .../tomcat/websocket/server/UpgradeUtil.java | 14 +++- .../server/TestUpgradeSingleValueHeaders.java | 93 ++++++++++++++++++++++ 2 files changed, 106 insertions(+), 1 deletion(-) diff --git a/java/org/apache/tomcat/websocket/server/UpgradeUtil.java b/java/org/apache/tomcat/websocket/server/UpgradeUtil.java index 2d3c12d004..13c6b26676 100644 --- a/java/org/apache/tomcat/websocket/server/UpgradeUtil.java +++ b/java/org/apache/tomcat/websocket/server/UpgradeUtil.java @@ -115,7 +115,14 @@ public class UpgradeUtil { resp.sendError(HttpServletResponse.SC_BAD_REQUEST); return; } - if (!headerContainsToken(req, Constants.WS_VERSION_HEADER_NAME, Constants.WS_VERSION_HEADER_VALUE)) { + // RFC 6455, sections 11.3.1 and 11.3.5: these header fields must not appear more than once in a request + if (countHeaders(req, Constants.WS_VERSION_HEADER_NAME) > 1 || + countHeaders(req, Constants.WS_KEY_HEADER_NAME) > 1) { + resp.sendError(HttpServletResponse.SC_BAD_REQUEST); + return; + } + String version = req.getHeader(Constants.WS_VERSION_HEADER_NAME); + if (version == null || !Constants.WS_VERSION_HEADER_VALUE.equals(version.trim())) { resp.setStatus(426); resp.setHeader(Constants.WS_VERSION_HEADER_NAME, Constants.WS_VERSION_HEADER_VALUE); return; @@ -330,6 +337,11 @@ public class UpgradeUtil { } + private static int countHeaders(HttpServletRequest req, String headerName) { + return Collections.list(req.getHeaders(headerName)).size(); + } + + /* * This only works for tokens. Quoted strings need more sophisticated parsing. */ diff --git a/test/org/apache/tomcat/websocket/server/TestUpgradeSingleValueHeaders.java b/test/org/apache/tomcat/websocket/server/TestUpgradeSingleValueHeaders.java new file mode 100644 index 0000000000..6110763ee4 --- /dev/null +++ b/test/org/apache/tomcat/websocket/server/TestUpgradeSingleValueHeaders.java @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.tomcat.websocket.server; + +import java.nio.charset.StandardCharsets; + +import jakarta.servlet.http.HttpServletResponse; +import jakarta.websocket.CloseReason.CloseCodes; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.tomcat.websocket.TesterEchoServer; +import org.apache.tomcat.websocket.WebSocketBaseTest; + +/** + * RFC 6455, sections 4.2.1, 11.3.1 and 11.3.5: the Sec-WebSocket-Key and Sec-WebSocket-Version header fields must + * appear once in the opening handshake request and the version must be 13. + */ +public class TestUpgradeSingleValueHeaders extends WebSocketBaseTest { + + private static final String KEY = "Sec-WebSocket-Key: MDEyMzQ1Njc4OTAxMjM0NQ==\r\n"; + private static final String VERSION = "Sec-WebSocket-Version: 13\r\n"; + + + @Test + public void testValid() throws Exception { + doTest(KEY + VERSION, HttpServletResponse.SC_SWITCHING_PROTOCOLS); + } + + + @Test + public void testDuplicateKey() throws Exception { + doTest(KEY + KEY + VERSION, HttpServletResponse.SC_BAD_REQUEST); + } + + + @Test + public void testDuplicateVersion() throws Exception { + doTest(KEY + VERSION + VERSION, HttpServletResponse.SC_BAD_REQUEST); + } + + + @Test + public void testVersionList() throws Exception { + doTest(KEY + "Sec-WebSocket-Version: 8, 13\r\n", 426); + } + + + @Test + public void testUnsupportedVersion() throws Exception { + doTest(KEY + "Sec-WebSocket-Version: 8\r\n", 426); + } + + + private void doTest(String keyAndVersionHeaders, int expectedStatusCode) throws Exception { + startServer(TesterEchoServer.Config.class); + + TesterWsClient client = new TesterWsClient("localhost", getPort()); + // Replace the key and version header lines of the default request + String req = client.createUpgradeRequest(TesterEchoServer.Config.PATH_BASIC) + .replaceAll("Sec-WebSocket-(Key|Version): [^\r]*\r\n", "") + .replace("Upgrade: websocket\r\n", "Upgrade: websocket\r\n" + keyAndVersionHeaders); + client.write(req.getBytes(StandardCharsets.UTF_8)); + int rc = client.readUpgradeResponse(); + + Assert.assertEquals(expectedStatusCode, rc); + + if (expectedStatusCode == HttpServletResponse.SC_SWITCHING_PROTOCOLS) { + client.sendCloseFrame(CloseCodes.NORMAL_CLOSURE); + // Read (and ignore) the response + byte[] buf = new byte[256]; + while (client.read(buf) > 0) { + // Ignore + } + } + client.closeSocket(); + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
