(tomcat) branch main updated: Add limit to HTTP response header size in upgrade to WebSocket

[email protected] Fri, 24 Jul 2026 16:12:51 +0000
Newsgroups gmane.comp.jakarta.tomcat.devel
Message-ID <178490957099.3891439.15658539377035291811@gitbox3-he-fi.apache.org>
This is an automated email from the ASF dual-hosted git repository.

markt-asf pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/tomcat.git


The following commit(s) were added to refs/heads/main by this push:
     new 67b01c13bc Add limit to HTTP response header size in upgrade to WebSocket
67b01c13bc is described below

commit 67b01c13bc489d5c8ce2b9e006ccb52161815316
Author: Mark Thomas <[email protected]>
AuthorDate: Fri Jul 24 17:12:38 2026 +0100

    Add limit to HTTP response header size in upgrade to WebSocket
---
 java/org/apache/tomcat/websocket/Constants.java    | 11 ++++++++++
 .../tomcat/websocket/LocalStrings.properties       |  1 +
 .../tomcat/websocket/WsWebSocketContainer.java     | 24 ++++++++++++++++++----
 webapps/docs/changelog.xml                         |  6 ++++++
 webapps/docs/web-socket-howto.xml                  |  7 +++++++
 5 files changed, 45 insertions(+), 4 deletions(-)

diff --git a/java/org/apache/tomcat/websocket/Constants.java b/java/org/apache/tomcat/websocket/Constants.java
index 7841f6b010..4e529b8b68 100644
--- a/java/org/apache/tomcat/websocket/Constants.java
+++ b/java/org/apache/tomcat/websocket/Constants.java
@@ -71,6 +71,17 @@ public class Constants {
      * Default I/O timeout in milliseconds for WebSocket client connections.
      */
     public static final long IO_TIMEOUT_MS_DEFAULT = 5000;
+    /**
+     * Property name to set to configure the maximum number of bytes that will be allowed for the HTTP response to a
+     * WebSocket handshake request. The default is {@link #MAX_HTTP_RESPONSE_HEADER_BYTES_DEFAULT}.
+     */
+    public static final String MAX_HTTP_RESPONSE_HEADER_BYTES_PROPERTY =
+            "org.apache.tomcat.websocket.MAX_HTTP_RESPONSE_HEADER_BYTES";
+    /**
+     * Default maximum number of bytes that will be allowed for the HTTP response to a
+     * WebSocket handshake request.
+     */
+    public static final long MAX_HTTP_RESPONSE_HEADER_BYTES_DEFAULT = 8192;
 
     // RFC 2068 recommended a limit of 5
     // Most browsers have a default limit of 20
diff --git a/java/org/apache/tomcat/websocket/LocalStrings.properties b/java/org/apache/tomcat/websocket/LocalStrings.properties
index f1f7c16e0d..a520f332cf 100644
--- a/java/org/apache/tomcat/websocket/LocalStrings.properties
+++ b/java/org/apache/tomcat/websocket/LocalStrings.properties
@@ -165,6 +165,7 @@ wsWebSocketContainer.pathWrongScheme=The scheme [{0}] is not supported. The supp
 wsWebSocketContainer.proxyConnectFail=Failed to connect to the configured Proxy [{0}]. The HTTP response code was [{1}]
 wsWebSocketContainer.redirectThreshold=Cyclic Location header [{0}] detected / reached max number of redirects [{1}] of max [{2}]
 wsWebSocketContainer.responseFail=The HTTP upgrade to WebSocket failed but partial data may have been received: Status Code [{0}], HTTP headers [{1}]
+wsWebSocketContainer.responseHeadersLimit=The HTTP upgrade to WebSocket failed because the size of the HTTP response headers exceeded the limit
 wsWebSocketContainer.sessionCloseFail=Session with ID [{0}] did not close cleanly
 wsWebSocketContainer.sslEngineFail=Unable to create SSLEngine to support SSL/TLS connections
 wsWebSocketContainer.unsupportedAuthScheme=Failed to handle HTTP response code [{0}]. Unsupported Authentication scheme [{1}] returned in response
diff --git a/java/org/apache/tomcat/websocket/WsWebSocketContainer.java b/java/org/apache/tomcat/websocket/WsWebSocketContainer.java
index 252e287677..495ef6c1b7 100644
--- a/java/org/apache/tomcat/websocket/WsWebSocketContainer.java
+++ b/java/org/apache/tomcat/websocket/WsWebSocketContainer.java
@@ -301,6 +301,13 @@ public class WsWebSocketContainer implements WebSocketContainer, BackgroundProce
         Transformation transformation = null;
         AsyncChannelWrapper channel = null;
 
+        long maxHttpResponseHeaderBytes = Constants.MAX_HTTP_RESPONSE_HEADER_BYTES_DEFAULT;
+        String maxHttpResponseHeaderBytesValue =
+                (String) userProperties.get(Constants.MAX_HTTP_RESPONSE_HEADER_BYTES_PROPERTY);
+        if (maxHttpResponseHeaderBytesValue != null) {
+            maxHttpResponseHeaderBytes = Long.parseLong(maxHttpResponseHeaderBytesValue);
+        }
+
         HandshakeResponse handshakeResponse = EMPTY_HANDSHAKE_RESPONSE;
         try {
             // Open the connection
@@ -311,7 +318,7 @@ public class WsWebSocketContainer implements WebSocketContainer, BackgroundProce
                 // Proxy CONNECT is clear text
                 channel = new AsyncChannelWrapperNonSecure(socketChannel);
                 writeRequest(channel, proxyConnect, timeout);
-                HttpResponse httpResponse = processResponse(response, channel, timeout);
+                HttpResponse httpResponse = processResponse(response, channel, timeout, maxHttpResponseHeaderBytes);
                 if (httpResponse.status == Constants.PROXY_AUTHENTICATION_REQUIRED) {
                     return processAuthenticationChallenge(clientEndpointHolder, clientEndpointConfiguration,
                             serverEndpointUri, redirectSet, userProperties, Method.CONNECT,
@@ -354,7 +361,7 @@ public class WsWebSocketContainer implements WebSocketContainer, BackgroundProce
             }
             writeRequest(channel, upgradeRequest, timeout);
 
-            HttpResponse httpResponse = processResponse(response, channel, timeout);
+            HttpResponse httpResponse = processResponse(response, channel, timeout, maxHttpResponseHeaderBytes);
 
             // Check maximum permitted redirects
             int maxRedirects = Constants.MAX_REDIRECTIONS_DEFAULT;
@@ -850,8 +857,9 @@ public class WsWebSocketContainer implements WebSocketContainer, BackgroundProce
      * @throws DeploymentException  if the response status line is not correctly formatted
      * @throws TimeoutException     if the response was not read within the expected timeout
      */
-    private HttpResponse processResponse(ByteBuffer response, AsyncChannelWrapper channel, long timeout)
-            throws InterruptedException, ExecutionException, DeploymentException, EOFException, TimeoutException {
+    private HttpResponse processResponse(ByteBuffer response, AsyncChannelWrapper channel, long timeout,
+            long maxHttpResponseHeaderBytes) throws InterruptedException, ExecutionException, DeploymentException,
+            EOFException, TimeoutException {
 
         Map<String,List<String>> headers = new CaseInsensitiveKeyMap<>();
 
@@ -860,6 +868,7 @@ public class WsWebSocketContainer implements WebSocketContainer, BackgroundProce
         boolean readHeaders = false;
         StringBuilder lineBuffer = new StringBuilder();
         String line = null;
+        long headerByteCount = 0;
         while (!readHeaders) {
             // On entering loop buffer will be empty and at the start of a new
             // loop the buffer will have been fully read.
@@ -879,6 +888,7 @@ public class WsWebSocketContainer implements WebSocketContainer, BackgroundProce
                 throw new EOFException(
                         sm.getString("wsWebSocketContainer.responseFail", Integer.toString(status), headers));
             }
+            headerByteCount += bytesRead.intValue();
             response.flip();
             while (response.hasRemaining() && !readHeaders) {
                 if (readLine(response, lineBuffer)) {
@@ -897,6 +907,12 @@ public class WsWebSocketContainer implements WebSocketContainer, BackgroundProce
                     line = null;
                 }
             }
+            if (readHeaders) {
+                headerByteCount -= response.remaining();
+            }
+            if (headerByteCount > maxHttpResponseHeaderBytes) {
+                throw new DeploymentException(sm.getString("wsWebSocketContainer.responseHeadersLimit"));
+            }
         }
 
         return new HttpResponse(status, new WsHandshakeResponse(headers));
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index fe15f828b2..3bf0614603 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -415,6 +415,12 @@
         <code>Writer</code> and <code>OutputStream</code>. (markt)
       </fix>
       <!-- Entries for backport and removal before 12.0.0-M1 below this line -->
+      <add>
+        Add a limit (defaults to 8KB) on the size of the HTTP response headers
+        accepted during a WebSocket HTTP upgrade. This is configured via the
+        <code>org.apache.tomcat.websocket.MAX_HTTP_RESPONSE_HEADER_BYTES</code>
+        user property. (markt)
+      </add>
     </changelog>
   </subsection>
   <subsection name="Web applications">
diff --git a/webapps/docs/web-socket-howto.xml b/webapps/docs/web-socket-howto.xml
index c159f1dc99..00d910b7e7 100644
--- a/webapps/docs/web-socket-howto.xml
+++ b/webapps/docs/web-socket-howto.xml
@@ -120,6 +120,13 @@
    timeout as a <code>String</code> in milliseconds. The default is 5000 (5
    seconds).</p>
 
+<p>When using the WebSocket client to connect to server endpoints, the maximum
+   number of bytes permitted in the HTTP response headers is controlled by the
+   <code>userProperties</code> of the provided
+   <code>jakarta.websocket.ClientEndpointConfig</code>. The property is
+   <code>org.apache.tomcat.websocket.MAX_HTTP_RESPONSE_HEADER_BYTES</code>. The
+   default is 8192 (8KB).</p>
+
 <p>When using the WebSocket client to connect to server endpoints, the number of
    HTTP redirects that the client will follow is controlled by the
    <code>userProperties</code> of the provided