This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 9.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 618fef554d0bc87913873d738e6638f2bbd6a9a4 Author: opencode <[email protected]> AuthorDate: Mon Oct 12 11:23:08 2026 +0200 Ignore empty entries in the remote IP header Entries of the comma-separated remote IP header were used as they came from the splitting, which trims whitespace but does not drop empty elements. A header value consisting only of whitespace or commas, or containing empty entries, ended up assigning an empty string as the remote address and remote host of the request, since an empty value is neither an internal nor a trusted proxy yet is not null. Downstream code then saw a blank client address in the access log, in access control valves and in the application. Depending on the internal proxy configuration, an empty entry could also be resolved as the loopback address and be classified as an internal proxy, which is another way in which an empty value should simply carry no meaning. Empty entries are now skipped in the loop that walks the header from right to left, and in the loop that rebuilds the header value, so an all-empty header leaves the request untouched as an absent header does, and a mixed header still yields the right-most real client address without echoing empty entries back. The valve and the filter that mirrors it gained a test for a whitespace-only header value. --- .../apache/catalina/filters/RemoteIpFilter.java | 9 +++++ java/org/apache/catalina/valves/RemoteIpValve.java | 9 +++++ .../catalina/filters/TestRemoteIpFilter.java | 32 +++++++++++++++++ .../apache/catalina/valves/TestRemoteIpValve.java | 40 ++++++++++++++++++++++ 4 files changed, 90 insertions(+) diff --git a/java/org/apache/catalina/filters/RemoteIpFilter.java b/java/org/apache/catalina/filters/RemoteIpFilter.java index 10532fef7a..b9b1aff51b 100644 --- a/java/org/apache/catalina/filters/RemoteIpFilter.java +++ b/java/org/apache/catalina/filters/RemoteIpFilter.java @@ -960,6 +960,12 @@ public class RemoteIpFilter extends GenericFilter { // loop on remoteIpHeaderValue to find the first trusted remote ip and to build the proxies chain for (idx = remoteIpHeaderValue.length - 1; idx >= 0; idx--) { String currentRemoteIp = remoteIpHeaderValue[idx]; + if (currentRemoteIp.isEmpty()) { + // Skip empty entries, such as those resulting from a + // whitespace-only header value, rather than ending up + // setting an empty remote address + continue; + } remoteIp = currentRemoteIp; if (internalProxies != null && internalProxies.matcher(currentRemoteIp).matches()) { // do nothing, internalProxies IPs are not appended to the @@ -974,6 +980,9 @@ public class RemoteIpFilter extends GenericFilter { LinkedList<String> newRemoteIpHeaderValue = new LinkedList<>(); for (; idx >= 0; idx--) { String currentRemoteIp = remoteIpHeaderValue[idx]; + if (currentRemoteIp.isEmpty()) { + continue; + } newRemoteIpHeaderValue.addFirst(currentRemoteIp); } diff --git a/java/org/apache/catalina/valves/RemoteIpValve.java b/java/org/apache/catalina/valves/RemoteIpValve.java index 7f0a191928..1eff20d42d 100644 --- a/java/org/apache/catalina/valves/RemoteIpValve.java +++ b/java/org/apache/catalina/valves/RemoteIpValve.java @@ -690,6 +690,12 @@ public class RemoteIpValve extends ValveBase { // loop on remoteIpHeaderValue to find the first trusted remote ip and to build the proxies chain for (idx = remoteIpHeaderValue.length - 1; idx >= 0; idx--) { String currentRemoteIp = remoteIpHeaderValue[idx]; + if (currentRemoteIp.isEmpty()) { + // Skip empty entries, such as those resulting from a + // whitespace-only header value, rather than ending up + // setting an empty remote address + continue; + } remoteIp = currentRemoteIp; if (internalProxies != null && internalProxies.matcher(currentRemoteIp).matches()) { // do nothing, internalProxies IPs are not appended to the @@ -704,6 +710,9 @@ public class RemoteIpValve extends ValveBase { Deque<String> newRemoteIpHeaderValue = new ArrayDeque<>(); for (; idx >= 0; idx--) { String currentRemoteIp = remoteIpHeaderValue[idx]; + if (currentRemoteIp.isEmpty()) { + continue; + } newRemoteIpHeaderValue.addFirst(currentRemoteIp); } if (remoteIp != null) { diff --git a/test/org/apache/catalina/filters/TestRemoteIpFilter.java b/test/org/apache/catalina/filters/TestRemoteIpFilter.java index e87d3d3ac9..a227404ada 100644 --- a/test/org/apache/catalina/filters/TestRemoteIpFilter.java +++ b/test/org/apache/catalina/filters/TestRemoteIpFilter.java @@ -284,6 +284,38 @@ public class TestRemoteIpFilter extends TomcatBaseTest { Assert.assertEquals("remoteHost", "remote-host-original-value", actualRemoteHost); } + /* + * A remote IP header value that contains nothing but whitespace (or commas) must leave the request untouched, + * just like an absent header, rather than setting an empty remote address. + */ + @Test + public void testInvokeAllowedRemoteAddrWithWhitespaceOnlyRemoteIpHeader() throws Exception { + // PREPARE + FilterDef filterDef = new FilterDef(); + filterDef.addInitParameter("internalProxies", "192\\.168\\.0\\.10|192\\.168\\.0\\.11"); + filterDef.addInitParameter("trustedProxies", "200.0.0.1,200.0.0.2,200.0.0.3"); + filterDef.addInitParameter("remoteIpHeader", "x-forwarded-for"); + filterDef.addInitParameter("proxiesHeader", "x-forwarded-by"); + + MockHttpServletRequest request = new MockHttpServletRequest(); + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("remote-host-original-value"); + request.addHeader("x-forwarded-for", " "); + + // TEST + HttpServletRequest actualRequest = testRemoteIpFilter(filterDef, request).getRequest(); + + // VERIFY + String actualXForwardedBy = actualRequest.getHeader("x-forwarded-by"); + Assert.assertNull("x-forwarded-by must not be added", actualXForwardedBy); + + String actualRemoteAddr = actualRequest.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "192.168.0.10", actualRemoteAddr); + + String actualRemoteHost = actualRequest.getRemoteHost(); + Assert.assertEquals("remoteHost", "remote-host-original-value", actualRemoteHost); + } + @Test public void testInvokeAllProxiesAreInternal() throws Exception { diff --git a/test/org/apache/catalina/valves/TestRemoteIpValve.java b/test/org/apache/catalina/valves/TestRemoteIpValve.java index 9616e0e571..35f9d9d235 100644 --- a/test/org/apache/catalina/valves/TestRemoteIpValve.java +++ b/test/org/apache/catalina/valves/TestRemoteIpValve.java @@ -151,6 +151,46 @@ public class TestRemoteIpValve { } + /* + * A remote IP header value that contains nothing but whitespace (or commas) must leave the request untouched, + * just like an absent header, rather than setting an empty remote address. + */ + @Test + public void testInvokeAllowedRemoteAddrWithWhitespaceOnlyRemoteIpHeader() throws Exception { + // PREPARE + RemoteIpValve remoteIpValve = new RemoteIpValve(); + remoteIpValve.setInternalProxies("192.168.0.10/31"); + remoteIpValve.setTrustedProxies("200.0.0.1, 200.0.0.2, 200.0.0.3"); + remoteIpValve.setRemoteIpHeader("x-forwarded-for"); + remoteIpValve.setProxiesHeader("x-forwarded-by"); + RemoteAddrAndHostTrackerValve remoteAddrAndHostTrackerValve = new RemoteAddrAndHostTrackerValve(); + remoteIpValve.setNext(remoteAddrAndHostTrackerValve); + + Request request = new MockRequest(new org.apache.coyote.Request()); + request.setRemoteAddr("192.168.0.10"); + request.setRemoteHost("remote-host-original-value"); + request.getCoyoteRequest().getMimeHeaders().addValue("x-forwarded-for").setString(" "); + + // TEST + remoteIpValve.invoke(request, null); + + // VERIFY + String actualXForwardedBy = remoteAddrAndHostTrackerValve.getForwardedBy(); + Assert.assertNull("x-forwarded-by must not be added", actualXForwardedBy); + + String actualRemoteAddr = remoteAddrAndHostTrackerValve.getRemoteAddr(); + Assert.assertEquals("remoteAddr", "192.168.0.10", actualRemoteAddr); + + String actualRemoteHost = remoteAddrAndHostTrackerValve.getRemoteHost(); + Assert.assertEquals("remoteHost", "remote-host-original-value", actualRemoteHost); + + String actualPostInvokeRemoteAddr = request.getRemoteAddr(); + Assert.assertEquals("postInvoke remoteAddr", "192.168.0.10", actualPostInvokeRemoteAddr); + + String actualPostInvokeRemoteHost = request.getRemoteHost(); + Assert.assertEquals("postInvoke remoteAddr", "remote-host-original-value", actualPostInvokeRemoteHost); + } + @Test public void testInvokeAllProxiesAreTrusted() throws Exception { --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
