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
The following commit(s) were added to refs/heads/9.0.x by this push:
new eaf1771d85 Re-work x-forwarded-proto multiple header fix
eaf1771d85 is described below
commit eaf1771d85cfd69d861051d088bdcf68f4869558
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 aac235dc53..f772c28cc2 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 600883c0f7..b418e33225 100644
--- a/java/org/apache/catalina/util/LocalStrings.properties
+++ b/java/org/apache/catalina/util/LocalStrings.properties
@@ -49,8 +49,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 ac944d4369..b9a003620e 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 javax.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 a67ee5cd05..bdb04eed3d 100644
--- a/java/org/apache/catalina/valves/RemoteIpValve.java
+++ b/java/org/apache/catalina/valves/RemoteIpValve.java
@@ -28,7 +28,6 @@ import java.util.List;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletResponse;
import org.apache.catalina.AccessLog;
import org.apache.catalina.Globals;
@@ -671,20 +670,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();
@@ -759,17 +744,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 cae446ad50..6e64dcc2c3 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -136,8 +136,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 1160fc9805..b3be5456c1 100644
--- a/webapps/docs/config/valve.xml
+++ b/webapps/docs/config/valve.xml
@@ -1172,6 +1172,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
@@ -1226,8 +1252,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">
lmpx.com only provides a reader for public news (NNTP) servers. It is not
affiliated with the servers or forums shown here and is not responsible for
the content of articles, which is written by their respective authors.