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 'x-forwarded-for'">
- <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 'x-forwarded-for'
and 'x-forwarded-proto'">
-
+ <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]