diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilter.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilter.java index 6fe61f7998..1df008f24d 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilter.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilter.java @@ -138,10 +138,11 @@ public int getOrder() { public HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange) { ServerHttpRequest request = exchange.getRequest(); - if (request.getRemoteAddress() != null - && !trustedProxies.isTrusted(request.getRemoteAddress().getHostString())) { + InetSocketAddress peerAddress = ForwardedHeadersFilterUtils.extractPeerRemoteAddress(request); + + if (peerAddress != null && !trustedProxies.isTrusted(peerAddress.getHostString())) { log.trace(LogMessage.format("Remote address not trusted. pattern %s remote address %s", trustedProxies, - request.getRemoteAddress())); + peerAddress)); return input; } @@ -177,25 +178,24 @@ public HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange) { forwarded.put("host", host); } - InetSocketAddress remoteAddress = request.getRemoteAddress(); // TODO: only add if "remoteAddress" value matches trustedProxies - if (remoteAddress != null) { + if (peerAddress != null) { // If remoteAddress is unresolved, calling getHostAddress() would cause a // NullPointerException. String forValue; - if (remoteAddress.isUnresolved()) { - forValue = remoteAddress.getHostName(); + if (peerAddress.isUnresolved()) { + forValue = peerAddress.getHostName(); } else { - InetAddress address = remoteAddress.getAddress(); - forValue = remoteAddress.getAddress().getHostAddress(); + InetAddress address = peerAddress.getAddress(); + forValue = peerAddress.getAddress().getHostAddress(); if (address instanceof Inet6Address) { forValue = "[" + forValue + "]"; } } if (trustedProxies.isTrusted(forValue)) { // only add for value if trusted - int port = remoteAddress.getPort(); + int port = peerAddress.getPort(); if (port >= 0) { forValue = forValue + ":" + port; } diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterUtils.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterUtils.java new file mode 100644 index 0000000000..6ff7293fb8 --- /dev/null +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterUtils.java @@ -0,0 +1,61 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter.headers; + +import java.net.InetSocketAddress; + +import org.jspecify.annotations.Nullable; + +import org.springframework.http.server.reactive.AbstractServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequestDecorator; + +/** + * Utility methods for forwarded headers filters. + * + * @author Dmitrii Grigorev + */ +public final class ForwardedHeadersFilterUtils { + + private ForwardedHeadersFilterUtils() { + } + + /** + * Get the real (peer) remote address by unwrapping to the native request when + * possible. + */ + public static @Nullable InetSocketAddress extractPeerRemoteAddress(ServerHttpRequest request) { + if (hasNativeRequest(request)) { + try { + ServerHttpRequest nativeRequest = ServerHttpRequestDecorator.getNativeRequest(request); + InetSocketAddress remoteAddress = nativeRequest.getRemoteAddress(); + if (remoteAddress != null) { + return remoteAddress; + } + } + catch (RuntimeException ignored) { + // e.g. MockServerHttpRequest extends AbstractServerHttpRequest but throws + } + } + return request.getRemoteAddress(); + } + + private static boolean hasNativeRequest(ServerHttpRequest request) { + return request instanceof ServerHttpRequestDecorator || request instanceof AbstractServerHttpRequest; + } + +} diff --git a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilter.java b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilter.java index 584cb6b1ca..e4aef97577 100644 --- a/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilter.java +++ b/spring-cloud-gateway-server-webflux/src/main/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilter.java @@ -16,6 +16,7 @@ package org.springframework.cloud.gateway.filter.headers; +import java.net.InetSocketAddress; import java.net.URI; import java.util.LinkedHashSet; import java.util.List; @@ -220,11 +221,11 @@ public void setPrefixAppend(boolean prefixAppend) { @Override public HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange) { ServerHttpRequest request = exchange.getRequest(); + InetSocketAddress peerAddress = ForwardedHeadersFilterUtils.extractPeerRemoteAddress(request); - if (request.getRemoteAddress() != null - && !trustedProxies.isTrusted(request.getRemoteAddress().getHostString())) { + if (peerAddress != null && !trustedProxies.isTrusted(peerAddress.getHostString())) { log.trace(LogMessage.format("Remote address not trusted. pattern %s remote address %s", trustedProxies, - request.getRemoteAddress())); + peerAddress)); return input; } @@ -237,8 +238,8 @@ public HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange) { if (isForEnabled()) { String remoteAddr = null; - if (request.getRemoteAddress() != null && request.getRemoteAddress().getAddress() != null) { - remoteAddr = request.getRemoteAddress().getHostString(); + if (peerAddress != null && peerAddress.getAddress() != null) { + remoteAddr = peerAddress.getHostString(); } // match xforwarded for against trusted proxies write(updated, X_FORWARDED_FOR_HEADER, remoteAddr, isForAppend(), trustedProxies::isTrusted); diff --git a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterUtilsTests.java b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterUtilsTests.java new file mode 100644 index 0000000000..2662359474 --- /dev/null +++ b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/headers/ForwardedHeadersFilterUtilsTests.java @@ -0,0 +1,131 @@ +/* + * Copyright 2026-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.cloud.gateway.filter.headers; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.Objects; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; + +import org.springframework.core.io.buffer.DataBuffer; +import org.springframework.http.HttpCookie; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.server.reactive.AbstractServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.SslInfo; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ForwardedHeadersFilterUtils}. + * + * @author Dmitrii Grigorev + */ +class ForwardedHeadersFilterUtilsTests { + + @Test + void extractRemoteAddressFromNativeRequest() throws Exception { + InetSocketAddress peerAddress = new InetSocketAddress(InetAddress.getByName("1.1.1.1"), 80); + ServerHttpRequest request = new TestServerHttpRequestWithNative(peerAddress); + + InetSocketAddress result = ForwardedHeadersFilterUtils.extractPeerRemoteAddress(request); + + assertThat(result).isNotNull(); + assertThat(result.getHostString()).isEqualTo("1.1.1.1"); + assertThat(result.getPort()).isEqualTo(80); + } + + @Test + void extractRemoteAddressFromNativeRequestOverrides() throws Exception { + InetSocketAddress peerAddress = new InetSocketAddress(InetAddress.getByName("1.1.1.1"), 80); + ServerHttpRequest nativeRequest = new TestServerHttpRequestWithNative(peerAddress); + + InetSocketAddress clientAddress = new InetSocketAddress(InetAddress.getByName("2.2.2.2"), 80); + + ServerHttpRequest.Builder builder = nativeRequest.mutate(); + // such behaviour is here: + // org.springframework.web.server.adapter.ForwardedHeaderTransformer + builder.remoteAddress(clientAddress); + ServerHttpRequest transformedRequest = builder.build(); + + InetSocketAddress result = ForwardedHeadersFilterUtils.extractPeerRemoteAddress(transformedRequest); + + assertThat(result).isNotNull(); + // the transformed request's remote address is overridden, we can't rely on it. + assertThat(Objects.requireNonNull(transformedRequest.getRemoteAddress()).getHostString()).isEqualTo("2.2.2.2"); + // only native request's has the real peer remote address + assertThat(result.getHostString()).isEqualTo("1.1.1.1"); + assertThat(result.getPort()).isEqualTo(80); + } + + @Test + void extractRemoteAddressNull() { + ServerHttpRequest request = MockServerHttpRequest.get("http://localhost/get").build(); + + InetSocketAddress result = ForwardedHeadersFilterUtils.extractPeerRemoteAddress(request); + + assertThat(result).isNull(); + } + + /** + * Minimal AbstractServerHttpRequest that exposes a native request with a peer remote + * address. + */ + private static final class TestServerHttpRequestWithNative extends AbstractServerHttpRequest { + + private final InetSocketAddress nativePeerAddress; + + TestServerHttpRequestWithNative(InetSocketAddress nativePeerAddress) { + super(HttpMethod.GET, URI.create("http://localhost/"), null, new HttpHeaders()); + this.nativePeerAddress = nativePeerAddress; + } + + @Override + protected MultiValueMap initCookies() { + return new LinkedMultiValueMap<>(); + } + + @Override + protected SslInfo initSslInfo() { + return null; + } + + @Override + public ServerHttpRequest getNativeRequest() { + return MockServerHttpRequest.get("http://localhost/").remoteAddress(this.nativePeerAddress).build(); + } + + @Override + public InetSocketAddress getRemoteAddress() { + return null; + } + + @Override + public Flux getBody() { + return Flux.empty(); + } + + } + +}