From cf8032c62d8a4352afd7ec09239c52d0312ee79e Mon Sep 17 00:00:00 2001 From: Mengqi Xu <2663479778@qq.com> Date: Sat, 29 Mar 2025 23:05:58 +0800 Subject: [PATCH] Add support for X-Forwarded-By and Forwarded By See gh-34654, gh-23260 Signed-off-by: Mengqi Xu <2663479778@qq.com> --- .../DefaultServerHttpRequestBuilder.java | 17 +++- .../server/reactive/ServerHttpRequest.java | 6 ++ .../web/filter/ForwardedHeaderFilter.java | 15 ++++ .../adapter/ForwardedHeaderTransformer.java | 7 ++ .../web/util/ForwardedHeaderUtils.java | 54 ++++++++++++ .../filter/ForwardedHeaderFilterTests.java | 87 +++++++++++++++++++ .../ForwardedHeaderTransformerTests.java | 36 ++++++++ 7 files changed, 219 insertions(+), 3 deletions(-) diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java index 1355ecd996de..41535e47c50f 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java @@ -57,6 +57,8 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder { private @Nullable InetSocketAddress remoteAddress; + private @Nullable InetSocketAddress localAddress; + private final Flux body; private final ServerHttpRequest originalRequest; @@ -131,10 +133,16 @@ public ServerHttpRequest.Builder remoteAddress(InetSocketAddress remoteAddress) return this; } + @Override + public ServerHttpRequest.Builder localAddress(InetSocketAddress localAddress) { + this.localAddress = localAddress; + return this; + } + @Override public ServerHttpRequest build() { return new MutatedServerHttpRequest(getUriToUse(), this.contextPath, - this.httpMethod, this.sslInfo, this.remoteAddress, this.headers, this.body, this.originalRequest); + this.httpMethod, this.sslInfo, this.remoteAddress, this.localAddress, this.headers, this.body, this.originalRequest); } private URI getUriToUse() { @@ -182,16 +190,19 @@ private static class MutatedServerHttpRequest extends AbstractServerHttpRequest private final @Nullable InetSocketAddress remoteAddress; + private final @Nullable InetSocketAddress localAddress; + private final Flux body; private final ServerHttpRequest originalRequest; public MutatedServerHttpRequest(URI uri, @Nullable String contextPath, - HttpMethod method, @Nullable SslInfo sslInfo, @Nullable InetSocketAddress remoteAddress, + HttpMethod method, @Nullable SslInfo sslInfo, @Nullable InetSocketAddress remoteAddress, @Nullable InetSocketAddress localAddress, HttpHeaders headers, Flux body, ServerHttpRequest originalRequest) { super(method, uri, contextPath, headers); this.remoteAddress = (remoteAddress != null ? remoteAddress : originalRequest.getRemoteAddress()); + this.localAddress = (localAddress != null ? localAddress : originalRequest.getLocalAddress()); this.sslInfo = (sslInfo != null ? sslInfo : originalRequest.getSslInfo()); this.body = body; this.originalRequest = originalRequest; @@ -204,7 +215,7 @@ protected MultiValueMap initCookies() { @Override public @Nullable InetSocketAddress getLocalAddress() { - return this.originalRequest.getLocalAddress(); + return this.localAddress; } @Override diff --git a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java index 249dfeea02fd..9e2d9d432cce 100644 --- a/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java +++ b/spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java @@ -184,6 +184,12 @@ interface Builder { */ Builder remoteAddress(InetSocketAddress remoteAddress); + /** + * Set the address of the local client. + * @since 7.x + */ + Builder localAddress(InetSocketAddress localAddress); + /** * Build a {@link ServerHttpRequest} decorator with the mutated properties. */ diff --git a/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java b/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java index 291232c6f2d3..ffc58f8029c6 100644 --- a/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java +++ b/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java @@ -73,6 +73,7 @@ * @author EddĂș MelĂ©ndez * @author Rob Winch * @author Brian Clozel + * @author Mengqi Xu * @since 4.3 * @see https://tools.ietf.org/html/rfc7239 * @see Forwarded Headers @@ -92,6 +93,7 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter { FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix"); FORWARDED_HEADER_NAMES.add("X-Forwarded-Ssl"); FORWARDED_HEADER_NAMES.add("X-Forwarded-For"); + FORWARDED_HEADER_NAMES.add("X-Forwarded-By"); } @@ -255,6 +257,8 @@ private static class ForwardedHeaderExtractingRequest extends ForwardedHeaderRem private final @Nullable InetSocketAddress remoteAddress; + private final @Nullable InetSocketAddress localAddress; + private final ForwardedPrefixExtractor forwardedPrefixExtractor; ForwardedHeaderExtractingRequest(HttpServletRequest servletRequest) { @@ -272,6 +276,7 @@ private static class ForwardedHeaderExtractingRequest extends ForwardedHeaderRem this.port = (port == -1 ? (this.secure ? 443 : 80) : port); this.remoteAddress = ForwardedHeaderUtils.parseForwardedFor(uri, headers, request.getRemoteAddress()); + this.localAddress = ForwardedHeaderUtils.parseForwardedBy(uri, headers, request.getLocalAddress()); // Use Supplier as Tomcat updates delegate request on FORWARD Supplier requestSupplier = () -> (HttpServletRequest) getRequest(); @@ -330,6 +335,16 @@ public int getRemotePort() { return (this.remoteAddress != null ? this.remoteAddress.getPort() : super.getRemotePort()); } + @Override + public @Nullable String getLocalAddr() { + return (this.localAddress != null ? this.localAddress.getHostString() : super.getLocalAddr()); + } + + @Override + public int getLocalPort() { + return (this.localAddress != null ? this.localAddress.getPort() : super.getLocalPort()); + } + @SuppressWarnings("DataFlowIssue") @Override public @Nullable Object getAttribute(String name) { diff --git a/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java b/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java index 9c0ef3913a67..f13e27d9391d 100644 --- a/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java +++ b/spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java @@ -55,6 +55,7 @@ * * @author Rossen Stoyanchev * @author Sebastien Deleuze + * @author Mengqi Xu * @since 5.1 * @see https://tools.ietf.org/html/rfc7239 * @see Forwarded Headers @@ -72,6 +73,7 @@ public class ForwardedHeaderTransformer implements FunctionRFC 7239, Section 5.1 + */ + public static @Nullable InetSocketAddress parseForwardedBy( + URI uri, HttpHeaders headers, @Nullable InetSocketAddress localAddress) { + + int port = (localAddress != null ? + localAddress.getPort() : "https".equals(uri.getScheme()) ? 443 : 80); + + String forwardedHeader = headers.getFirst("Forwarded"); + if (StringUtils.hasText(forwardedHeader)) { + String forwardedToUse = StringUtils.tokenizeToStringArray(forwardedHeader, ",")[0]; + Matcher matcher = FORWARDED_BY_PATTERN.matcher(forwardedToUse); + if (matcher.find()) { + String value = matcher.group(1).trim(); + String host = value; + int portSeparatorIdx = value.lastIndexOf(':'); + int squareBracketIdx = value.lastIndexOf(']'); + if (portSeparatorIdx > squareBracketIdx) { + if (squareBracketIdx == -1 && value.indexOf(':') != portSeparatorIdx) { + throw new IllegalArgumentException("Invalid IPv4 address: " + value); + } + host = value.substring(0, portSeparatorIdx); + try { + port = Integer.parseInt(value, portSeparatorIdx + 1, value.length(), 10); + } + catch (NumberFormatException ex) { + throw new IllegalArgumentException( + "Failed to parse a port from \"forwarded\"-type header value: " + value); + } + } + return InetSocketAddress.createUnresolved(host, port); + } + } + + String byHeader = headers.getFirst("X-Forwarded-By"); + if (StringUtils.hasText(byHeader)) { + String host = StringUtils.tokenizeToStringArray(byHeader, ",")[0]; + boolean ipv6 = (host.indexOf(':') != -1); + host = (ipv6 && !host.startsWith("[") && !host.endsWith("]") ? "[" + host + "]" : host); + return InetSocketAddress.createUnresolved(host, port); + } + + return null; + } + } diff --git a/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java b/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java index f34a754c99b7..b5e1a13a7470 100644 --- a/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java +++ b/spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java @@ -49,6 +49,7 @@ * @author Rob Winch * @author Brian Clozel * @author Sebastien Deleuze + * @author Mengqi Xu */ class ForwardedHeaderFilterTests { @@ -66,6 +67,8 @@ class ForwardedHeaderFilterTests { private static final String X_FORWARDED_FOR = "x-forwarded-for"; + private static final String X_FORWARDED_BY = "x-forwarded-by"; + private final ForwardedHeaderFilter filter = new ForwardedHeaderFilter(); @@ -93,6 +96,7 @@ void shouldFilter() { testShouldFilter(X_FORWARDED_SSL); testShouldFilter(X_FORWARDED_PREFIX); testShouldFilter(X_FORWARDED_FOR); + testShouldFilter(X_FORWARDED_BY); } private void testShouldFilter(String headerName) { @@ -115,6 +119,7 @@ void forwardedRequest(String protocol) throws Exception { this.request.addHeader(X_FORWARDED_PORT, "443"); this.request.addHeader("foo", "bar"); this.request.addHeader(X_FORWARDED_FOR, "[203.0.113.195]"); + this.request.addHeader(X_FORWARDED_BY, "[203.0.113.196]"); this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain); HttpServletRequest actual = (HttpServletRequest) this.filterChain.getRequest(); @@ -126,11 +131,13 @@ void forwardedRequest(String protocol) throws Exception { assertThat(actual.getServerPort()).isEqualTo(443); assertThat(actual.isSecure()).isTrue(); assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("[203.0.113.195]"); + assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("[203.0.113.196]"); assertThat(actual.getHeader(X_FORWARDED_PROTO)).isNull(); assertThat(actual.getHeader(X_FORWARDED_HOST)).isNull(); assertThat(actual.getHeader(X_FORWARDED_PORT)).isNull(); assertThat(actual.getHeader(X_FORWARDED_FOR)).isNull(); + assertThat(actual.getHeader(X_FORWARDED_BY)).isNull(); assertThat(actual.getHeader("foo")).isEqualTo("bar"); } @@ -143,6 +150,7 @@ void forwardedRequestInRemoveOnlyMode() throws Exception { this.request.addHeader(X_FORWARDED_SSL, "on"); this.request.addHeader("foo", "bar"); this.request.addHeader(X_FORWARDED_FOR, "203.0.113.195"); + this.request.addHeader(X_FORWARDED_BY, "203.0.113.196"); this.filter.setRemoveOnly(true); this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain); @@ -156,12 +164,14 @@ void forwardedRequestInRemoveOnlyMode() throws Exception { assertThat(actual.isSecure()).isFalse(); assertThat(actual.getRemoteAddr()).isEqualTo(MockHttpServletRequest.DEFAULT_REMOTE_ADDR); assertThat(actual.getRemoteHost()).isEqualTo(MockHttpServletRequest.DEFAULT_REMOTE_HOST); + assertThat(actual.getLocalAddr()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_ADDR); assertThat(actual.getHeader(X_FORWARDED_PROTO)).isNull(); assertThat(actual.getHeader(X_FORWARDED_HOST)).isNull(); assertThat(actual.getHeader(X_FORWARDED_PORT)).isNull(); assertThat(actual.getHeader(X_FORWARDED_SSL)).isNull(); assertThat(actual.getHeader(X_FORWARDED_FOR)).isNull(); + assertThat(actual.getHeader(X_FORWARDED_BY)).isNull(); assertThat(actual.getHeader("foo")).isEqualTo("bar"); } @@ -541,6 +551,83 @@ void forwardedForMultipleIdentifiers() throws Exception { } + @Nested + class ForwardedBy { + + @Test + void xForwardedForEmpty() throws Exception { + request.addHeader(X_FORWARDED_BY, ""); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getLocalAddr()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_ADDR); + assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT); + } + + @Test + void xForwardedForSingleIdentifier() throws Exception { + request.addHeader(X_FORWARDED_BY, "203.0.113.195"); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("203.0.113.195"); + assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT); + } + + @Test + void xForwardedForMultipleIdentifiers() throws Exception { + request.addHeader(X_FORWARDED_BY, "203.0.113.195, 70.41.3.18, 150.172.238.178"); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("203.0.113.195"); + assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT); + } + + @Test + void forwardedForIpV4Identifier() throws Exception { + request.addHeader(FORWARDED, "By=203.0.113.195"); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("203.0.113.195"); + assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT); + } + + @Test + void forwardedForIpV6Identifier() throws Exception { + request.addHeader(FORWARDED, "By=\"[2001:db8:cafe::17]\""); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("[2001:db8:cafe::17]"); + assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT); + } + + @Test + void forwardedForIpV4IdentifierWithPort() throws Exception { + request.addHeader(FORWARDED, "By=\"203.0.113.195:47011\""); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("203.0.113.195"); + assertThat(actual.getLocalPort()).isEqualTo(47011); + } + + @Test + void forwardedForIpV6IdentifierWithPort() throws Exception { + request.addHeader(FORWARDED, "By=\"[2001:db8:cafe::17]:47011\""); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("[2001:db8:cafe::17]"); + assertThat(actual.getLocalPort()).isEqualTo(47011); + } + + @Test + void forwardedForMultipleIdentifiers() throws Exception { + request.addHeader(FORWARDED, "by=203.0.113.195;proto=http, by=\"[2001:db8:cafe::17]\", by=unknown"); + HttpServletRequest actual = filterAndGetWrappedRequest(); + + assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("203.0.113.195"); + assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT); + } + + } + @Nested class SendRedirect { diff --git a/spring-web/src/test/java/org/springframework/web/server/adapter/ForwardedHeaderTransformerTests.java b/spring-web/src/test/java/org/springframework/web/server/adapter/ForwardedHeaderTransformerTests.java index f966c099b608..d4dbe0c5200d 100644 --- a/spring-web/src/test/java/org/springframework/web/server/adapter/ForwardedHeaderTransformerTests.java +++ b/spring-web/src/test/java/org/springframework/web/server/adapter/ForwardedHeaderTransformerTests.java @@ -33,6 +33,7 @@ * * @author Rossen Stoyanchev * @author Sebastien Deleuze + * @author Mengqi Xu */ class ForwardedHeaderTransformerTests { @@ -52,6 +53,7 @@ void removeOnly() { headers.add("X-Forwarded-Prefix", "prefix"); headers.add("X-Forwarded-Ssl", "on"); headers.add("X-Forwarded-For", "203.0.113.195"); + headers.add("X-Forwarded-By", "203.0.113.196"); ServerHttpRequest request = this.requestMutator.apply(getRequest(headers)); assertForwardedHeadersRemoved(request); @@ -233,6 +235,40 @@ void xForwardedFor() { assertThat(request.getRemoteAddress().getHostName()).isEqualTo("203.0.113.195"); } + @Test + void forwardedBy() { + HttpHeaders headers = new HttpHeaders(); + headers.add("Forwarded", "by=\"203.0.113.195:4711\";host=84.198.58.199;proto=https"); + + InetSocketAddress localAddress = new InetSocketAddress("example.client", 47011); + + ServerHttpRequest request = MockServerHttpRequest + .method(HttpMethod.GET, URI.create("https://example.com/a%20b?q=a%2Bb")) + .localAddress(localAddress) + .headers(headers) + .build(); + + request = this.requestMutator.apply(request); + assertThat(request.getLocalAddress()).isNotNull(); + assertThat(request.getLocalAddress().getHostName()).isEqualTo("203.0.113.195"); + assertThat(request.getLocalAddress().getPort()).isEqualTo(4711); + } + + @Test + void xForwardedBy() { + HttpHeaders headers = new HttpHeaders(); + headers.add("x-forwarded-by", "203.0.113.195, 70.41.3.18, 150.172.238.178"); + + ServerHttpRequest request = MockServerHttpRequest + .method(HttpMethod.GET, URI.create("https://example.com/a%20b?q=a%2Bb")) + .headers(headers) + .build(); + + request = this.requestMutator.apply(request); + assertThat(request.getLocalAddress()).isNotNull(); + assertThat(request.getLocalAddress().getHostName()).isEqualTo("203.0.113.195"); + } + private MockServerHttpRequest getRequest(HttpHeaders headers) { return MockServerHttpRequest.get(BASE_URL).headers(headers).build();