This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 10.1.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit f49637821411fc0a7e4c9aec8d7e30f32578c767 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 fbec035d46..8a6f2796e1 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 3018fca4a7..f385825817 100644 --- a/java/org/apache/catalina/valves/RemoteIpValve.java +++ b/java/org/apache/catalina/valves/RemoteIpValve.java @@ -661,6 +661,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 @@ -675,6 +681,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 7b39045ed6..e74cf01ed3 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 c70b7f2ea6..30338efae4 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]
