This is an automated email from the ASF dual-hosted git repository.

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


The following commit(s) were added to refs/heads/10.1.x by this push:
     new 5e340e73cc Re-work x-forwarded-proto multiple header fix
5e340e73cc is described below

commit 5e340e73cc9c25d21be857481f2e1e3033d7dedf
Author: Mark Thomas <[email protected]>
AuthorDate: Thu Aug 20 09:44:50 2026 +0100

    Re-work x-forwarded-proto multiple header fix
---
 .../catalina/filters/LocalStrings.properties       |  1 -
 .../apache/catalina/filters/RemoteIpFilter.java    | 14 +----
 .../apache/catalina/util/LocalStrings.properties   |  2 -
 java/org/apache/catalina/util/RequestUtil.java     | 39 ++++++-------
 .../apache/catalina/valves/LocalStrings.properties |  1 -
 java/org/apache/catalina/valves/RemoteIpValve.java | 40 +++++--------
 webapps/docs/changelog.xml                         |  5 +-
 webapps/docs/config/filter.xml                     | 65 +++++++++++++---------
 webapps/docs/config/valve.xml                      | 35 +++++++++++-
 9 files changed, 108 insertions(+), 94 deletions(-)

diff --git a/java/org/apache/catalina/filters/LocalStrings.properties 
b/java/org/apache/catalina/filters/LocalStrings.properties
index 26ef3f72ea..18b468dd74 100644
--- a/java/org/apache/catalina/filters/LocalStrings.properties
+++ b/java/org/apache/catalina/filters/LocalStrings.properties
@@ -78,7 +78,6 @@ remoteIpFilter.invalidHostWithPort=Host value [{0}] in HTTP 
header [{1}] include
 remoteIpFilter.invalidNumber=Illegal number for parameter [{0}]: [{1}]
 remoteIpFilter.invalidPort=Port [{0}] in HTTP header [{1}] included a port 
number which will be ignored
 remoteIpFilter.invalidRemoteAddress=Unable to determine the remote host 
because the reported remote address [{0}] is not valid
-remoteIpFilter.multipleHeaders=Multiple [{0}] headers found in HTTP headers 
when only one is expected
 
 requestFilter.deny=Denied request for [{0}] based on property [{1}]
 
diff --git a/java/org/apache/catalina/filters/RemoteIpFilter.java 
b/java/org/apache/catalina/filters/RemoteIpFilter.java
index d7c7522965..71ef01975a 100644
--- a/java/org/apache/catalina/filters/RemoteIpFilter.java
+++ b/java/org/apache/catalina/filters/RemoteIpFilter.java
@@ -1013,18 +1013,8 @@ public class RemoteIpFilter extends GenericFilter {
                 }
             }
 
-            if (protocolHeader != null) {
-                String protocolHeaderValue;
-                try {
-                    protocolHeaderValue = RequestUtil.getUniqueHeader(request, 
protocolHeader);
-                } catch (IllegalArgumentException iae) {
-                    if (log.isDebugEnabled()) {
-                        
log.debug(sm.getString("remoteIpFilter.multipleHeaders", protocolHeader));
-                    }
-                    response.sendError(HttpServletResponse.SC_BAD_REQUEST);
-                    return;
-                }
-
+            if (protocolHeader != null && !protocolHeader.isEmpty()) {
+                String protocolHeaderValue = 
RequestUtil.getMergedHeaderValue(request, protocolHeader);
                 if (protocolHeaderValue == null) {
                     // Don't modify the secure, scheme and serverPort 
attributes
                     // of the request
diff --git a/java/org/apache/catalina/util/LocalStrings.properties 
b/java/org/apache/catalina/util/LocalStrings.properties
index 50f828397a..4e7774d0ca 100644
--- a/java/org/apache/catalina/util/LocalStrings.properties
+++ b/java/org/apache/catalina/util/LocalStrings.properties
@@ -44,8 +44,6 @@ netmask.invalidPort=The port part in the pattern [{0}] is not 
valid
 
 parameterMap.locked=No modifications are allowed to a locked ParameterMap
 
-requestUtil.multipleHeaders=Multiple [{0}] headers found in HTTP headers when 
only one is expected
-
 resourceSet.locked=No modifications are allowed to a locked ResourceSet
 
 sessionIdGeneratorBase.createRandom=Creation of SecureRandom instance for 
session ID generation using [{0}] took [{1}] milliseconds.
diff --git a/java/org/apache/catalina/util/RequestUtil.java 
b/java/org/apache/catalina/util/RequestUtil.java
index 2be4f68844..9b78425d39 100644
--- a/java/org/apache/catalina/util/RequestUtil.java
+++ b/java/org/apache/catalina/util/RequestUtil.java
@@ -22,15 +22,11 @@ import java.util.Enumeration;
 import jakarta.servlet.http.HttpServletRequest;
 
 import org.apache.catalina.connector.Request;
-import org.apache.tomcat.util.res.StringManager;
 
 /**
  * General purpose request parsing and encoding utility methods.
  */
 public final class RequestUtil {
-
-    private static final StringManager sm = 
StringManager.getManager(RequestUtil.class);
-
     /**
      * Default constructor.
      */
@@ -121,7 +117,7 @@ public final class RequestUtil {
      * @param url     The URL to test
      *
      * @return {@code true} if the provided URL is for a resource contained 
within the same web application as the
-     * request, otherwise {@code false}
+     *             request, otherwise {@code false}
      */
     public static boolean isSameWebApplication(HttpServletRequest request, URL 
url) {
         // Does this URL match down to (and including) the context path?
@@ -164,25 +160,26 @@ public final class RequestUtil {
 
 
     /**
-     * Obtains an HTTP value, ensuring that there is no more than one instance 
of the header.
-     *
-     * @param request    The request from which to obtain the HTTP headers
-     * @param headerName The name of the required HTTP header
+     * Behaves the same way as {@link HttpServletRequest#getHeader(String)} 
but with the addition that, if multiple
+     * headers of the specified name are present, the values are concatenated 
(with commas) before returning a single
+     * combined value.
      *
-     * @return The value for the HTTP header of there is exactly one instance 
of the header in the request. {@code null}
-     * if there are zero instances of the header
+     * @param request    The request from which the header value(s) should be 
retrieved
+     * @param headerName The name of the HTTP header for which the merged 
value should be obtained
      *
-     * @throws IllegalArgumentException if there is more than one instance of 
the header in the request
+     * @return The merged value for the given HTTP header.
      */
-    public static String getUniqueHeader(HttpServletRequest request, String 
headerName) {
-        Enumeration<String> headerValues = request.getHeaders(headerName);
-        String value = null;
-        if (headerValues.hasMoreElements()) {
-            value = headerValues.nextElement();
-            if (headerValues.hasMoreElements()) {
-                throw new 
IllegalArgumentException(sm.getString("requestUtil.multipleHeaders", 
headerName));
-            }
+    public static String getMergedHeaderValue(HttpServletRequest request, 
String headerName) {
+        Enumeration<String> values = request.getHeaders(headerName);
+        if (!values.hasMoreElements()) {
+            return null;
+        }
+        StringBuilder result = new StringBuilder();
+        result.append(values.nextElement());
+        while (values.hasMoreElements()) {
+            result.append(',');
+            result.append(values.nextElement());
         }
-        return value;
+        return result.toString();
     }
 }
diff --git a/java/org/apache/catalina/valves/LocalStrings.properties 
b/java/org/apache/catalina/valves/LocalStrings.properties
index f50271463a..5b8053a6a4 100644
--- a/java/org/apache/catalina/valves/LocalStrings.properties
+++ b/java/org/apache/catalina/valves/LocalStrings.properties
@@ -163,7 +163,6 @@ remoteIpValve.invalidHostHeader=Invalid value [{0}] found 
for Host in HTTP heade
 remoteIpValve.invalidHostWithPort=Host value [{0}] in HTTP header [{1}] 
included a port number which will be ignored
 remoteIpValve.invalidPortHeader=Invalid value [{0}] found for port in HTTP 
header [{1}]
 remoteIpValve.invalidRemoteAddress=Unable to determine the remote host because 
the reported remote address [{0}] is not valid
-remoteIpValve.multipleHeaders=Multiple [{0}] headers found in HTTP headers 
when only one is expected
 
 requestFilterValve.configInvalid=One or more invalid configuration settings 
were provided for the Remote[Addr|Host]Valve which prevented the Valve and its 
parent containers from starting
 requestFilterValve.deny=Denied request for [{0}] based on property [{1}]
diff --git a/java/org/apache/catalina/valves/RemoteIpValve.java 
b/java/org/apache/catalina/valves/RemoteIpValve.java
index 10f77cfb80..126171369c 100644
--- a/java/org/apache/catalina/valves/RemoteIpValve.java
+++ b/java/org/apache/catalina/valves/RemoteIpValve.java
@@ -27,7 +27,6 @@ import java.util.List;
 import java.util.regex.Pattern;
 
 import jakarta.servlet.ServletException;
-import jakarta.servlet.http.HttpServletResponse;
 
 import org.apache.catalina.AccessLog;
 import org.apache.catalina.Globals;
@@ -642,20 +641,6 @@ public class RemoteIpValve extends ValveBase {
         }
 
         if (isInternal || (trustedProxies != null && 
trustedProxies.matcher(originalRemoteAddr).matches())) {
-            // Validate before request modifications
-            String protocolHeaderValue = null;
-            if (protocolHeader != null) {
-                try {
-                    protocolHeaderValue = RequestUtil.getUniqueHeader(request, 
protocolHeader);
-                } catch (IllegalArgumentException iae) {
-                    if (log.isDebugEnabled()) {
-                        
log.debug(sm.getString("remoteIpValve.multipleHeaders", protocolHeader), iae);
-                    }
-                    response.sendError(HttpServletResponse.SC_BAD_REQUEST);
-                    return;
-                }
-            }
-
             String remoteIp = null;
             Deque<String> proxiesHeaderValue = new ArrayDeque<>();
             StringBuilder concatRemoteIpHeaderValue = new StringBuilder();
@@ -730,17 +715,20 @@ public class RemoteIpValve extends ValveBase {
                 }
             }
 
-            if (protocolHeaderValue == null) {
-                // Don't modify the secure, scheme and serverPort attributes
-                // of the request
-            } else if (isForwardedProtoHeaderValueSecure(protocolHeaderValue)) 
{
-                request.setSecure(true);
-                request.getCoyoteRequest().scheme().setString("https");
-                setPorts(request, httpsServerPort);
-            } else {
-                request.setSecure(false);
-                request.getCoyoteRequest().scheme().setString("http");
-                setPorts(request, httpServerPort);
+            if (protocolHeader != null && !protocolHeader.isEmpty()) {
+                String protocolHeaderValue = 
RequestUtil.getMergedHeaderValue(request, protocolHeader);
+                if (protocolHeaderValue == null) {
+                    // Don't modify the secure, scheme and serverPort 
attributes
+                    // of the request
+                } else if 
(isForwardedProtoHeaderValueSecure(protocolHeaderValue)) {
+                    request.setSecure(true);
+                    request.getCoyoteRequest().scheme().setString("https");
+                    setPorts(request, httpsServerPort);
+                } else {
+                    request.setSecure(false);
+                    request.getCoyoteRequest().scheme().setString("http");
+                    setPorts(request, httpServerPort);
+                }
             }
 
             if (hostHeader != null) {
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 480404bd76..abcc7a7447 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -127,8 +127,9 @@
         (markt)
       </fix>
       <add>
-        Reject requests containing multiple protocol header values in the
-        <code>RemoteIpFilter</code> and <code>RemoteIpValve</code>. (markt)
+        Support multiple protocol header values (treat as a single merged 
header
+        value) in the <code>RemoteIpFilter</code> and
+        <code>RemoteIpValve</code>. (markt)
       </add>
     </changelog>
   </subsection>
diff --git a/webapps/docs/config/filter.xml b/webapps/docs/config/filter.xml
index 1dc26cf474..ef53e37abc 100644
--- a/webapps/docs/config/filter.xml
+++ b/webapps/docs/config/filter.xml
@@ -1531,36 +1531,42 @@ FINE: Request "/docs/config/manager.html" with response 
status "200"
 
   </subsection>
 
-  <subsection name="Basic configuration to handle &#x27;x-forwarded-for&#x27;">
-    <p>
-    The filter will process the <code>x-forwarded-for</code> http header.
-    </p>
-    <source><![CDATA[      <filter>
-        <filter-name>RemoteIpFilter</filter-name>
-        <filter-class>org.apache.catalina.filters.RemoteIpFilter</filter-class>
-      </filter>
-
-      <filter-mapping>
-        <filter-name>RemoteIpFilter</filter-name>
-        <url-pattern>/*</url-pattern>
-        <dispatcher>REQUEST</dispatcher>
-      </filter-mapping>]]></source>
-  </subsection>
-
-  <subsection name="Basic configuration to handle &#x27;x-forwarded-for&#x27; 
and &#x27;x-forwarded-proto&#x27;">
-
+  <subsection name="Security">
+
+    <p>The filter changes security sensitive properties of the request based on
+       values provided in HTTP headers. It is essential that Tomcat is able to
+       trust these values for secure operation. Therefore, the first proxy
+       (trusted or internal) must ensure that any of the HTTP headers used by
+       the filter are removed from the untrusted request received.</p>
+    <p>Only the HTTP headers used by the filter need to be removed. The names 
of
+       those headers will vary depending on configuration. Assuming default
+       header names (adjust as necessary if non-default header names are used),
+       the headers that need to be removed are:</p>
+       <ul>
+         <li><code>x-forwarded-for</code></li>
+         <li><code>x-forwarded-by</code></li>
+         <li><code>x-forwarded-proto</code></li>
+         <li><code>x-forwarded-host</code></li>
+         <li><code>x-forwarded-port</code></li>
+       </ul>
+     <p>Note that HTTP header names are case-insensitive.</p>
+
+     <p>The default value for <code>internalProxies</code> assumes that all
+        local networks are trusted. If this is not the case, adjust the value
+        accordingly.</p>
+
+  </subsection>
+
+  <subsection name="Basic configuration">
     <p>
     The filter will process <code>x-forwarded-for</code> and
-    <code>x-forwarded-proto</code> http headers. Expected value for the
+    <code>x-forwarded-proto</code> HTTP headers. Expected value for the
     <code>x-forwarded-proto</code> header in case of SSL connections is
-    <code>https</code> (case insensitive). </p>
+    <code>https</code> (case insensitive).
+    </p>
     <source><![CDATA[      <filter>
         <filter-name>RemoteIpFilter</filter-name>
         <filter-class>org.apache.catalina.filters.RemoteIpFilter</filter-class>
-        <init-param>
-          <param-name>protocolHeader</param-name>
-          <param-value>x-forwarded-proto</param-value>
-        </init-param>
       </filter>
 
       <filter-mapping>
@@ -1890,9 +1896,14 @@ FINE: Request "/docs/config/manager.html" with response 
status "200"
       </attribute>
 
       <attribute name="protocolHeader" required="false">
-        <p>Name of the HTTP Header read by this valve that holds the protocol
-        used by the client to connect to the proxy. If not specified, the
-        default of <code>X-Forwarded-Proto</code> is used.</p>
+        <p>Name of the HTTP Header read by this filter that holds the protocol
+        used by the client to connect to the proxy. Multiple headers with the
+        same name will be merged as per RFC 9110, section 5.2. If multiple
+        values are present they must all match
+        <code>protocolHeaderHttpsValue</code> for the request to be treated as
+        an HTTPS request else it will be treated as HTTP. If not specified, the
+        default of <code>X-Forwarded-Proto</code> is used. To disable this
+        feature set the value to the empty string.</p>
       </attribute>
 
       <attribute name="hostHeader" required="false">
diff --git a/webapps/docs/config/valve.xml b/webapps/docs/config/valve.xml
index b7127d935b..d3c35796f3 100644
--- a/webapps/docs/config/valve.xml
+++ b/webapps/docs/config/valve.xml
@@ -1177,6 +1177,32 @@
 
   </subsection>
 
+  <subsection name="Security">
+
+    <p>The valve changes security sensitive properties of the request based on
+       values provided in HTTP headers. It is essential that Tomcat is able to
+       trust these values for secure operation. Therefore, the first proxy
+       (trusted or internal) must ensure that any of the HTTP headers used by
+       the valve are removed from the untrusted request received.</p>
+    <p>Only the HTTP headers used by the valve need to be removed. The names of
+       those headers will vary depending on configuration. Assuming default
+       header names (adjust as necessary if non-default header names are used),
+       the headers that need to be removed are:</p>
+       <ul>
+         <li><code>x-forwarded-for</code></li>
+         <li><code>x-forwarded-by</code></li>
+         <li><code>x-forwarded-proto</code></li>
+         <li><code>x-forwarded-host</code></li>
+         <li><code>x-forwarded-port</code></li>
+       </ul>
+     <p>Note that HTTP header names are case-insensitive.</p>
+
+     <p>The default value for <code>internalProxies</code> assumes that all
+        local networks are trusted. If this is not the case, adjust the value
+        accordingly.</p>
+
+  </subsection>
+
   <subsection name="Attributes">
 
     <p>The <strong>Remote IP Valve</strong> supports the
@@ -1231,8 +1257,13 @@
 
       <attribute name="protocolHeader" required="false">
         <p>Name of the HTTP Header read by this valve that holds the protocol
-        used by the client to connect to the proxy. If not specified, the
-        default of <code>X-Forwarded-Proto</code> is used.</p>
+        used by the client to connect to the proxy. Multiple headers with the
+        same name will be merged as per RFC 9110, section 5.2. If multiple
+        values are present they must all match
+        <code>protocolHeaderHttpsValue</code> for the request to be treated as
+        an HTTPS request else it will be treated as HTTP. If not specified, the
+        default of <code>X-Forwarded-Proto</code> is used. To disable this
+        feature set the value to the empty string.</p>
       </attribute>
 
       <attribute name="hostHeader" required="false">


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to