diff --git a/spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilter.java b/spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilter.java index 4ef283ca8..960575e52 100644 --- a/spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilter.java +++ b/spring-cloud-gateway-server-webmvc/src/main/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilter.java @@ -138,20 +138,22 @@ public HttpHeaders apply(HttpHeaders input, ServerRequest request) { MvcUtils.GATEWAY_ORIGINAL_REQUEST_URL_ATTR); URI requestUri = request.uri(); - if (originalUris != null && requestUri != null) { + if (originalUris != null && !originalUris.isEmpty() && requestUri != null) { - originalUris.forEach(originalUri -> { + // only the first uri counts: it is the url as received from the client. + // later entries are intermediate snapshots added by each path-mutating + // filter and would produce overlapping prefixes (gh-4237) + URI originalUri = originalUris.iterator().next(); - if (originalUri != null && originalUri.getPath() != null) { - // strip trailing slashes before checking if request path is end - // of original path - String originalUriPath = stripTrailingSlash(originalUri); - String requestUriPath = stripTrailingSlash(requestUri); + if (originalUri != null && originalUri.getPath() != null) { + // strip trailing slashes before checking if request path is end + // of original path + String originalUriPath = stripTrailingSlash(originalUri); + String requestUriPath = stripTrailingSlash(requestUri); - updateRequest(updated, originalUri, originalUriPath, requestUriPath); + updateRequest(updated, originalUri, originalUriPath, requestUriPath); - } - }); + } } } diff --git a/spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilterTests.java b/spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilterTests.java index 28846ba0a..741599033 100644 --- a/spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilterTests.java +++ b/spring-cloud-gateway-server-webmvc/src/test/java/org/springframework/cloud/gateway/server/mvc/filter/XForwardedRequestHeadersFilterTests.java @@ -16,6 +16,7 @@ package org.springframework.cloud.gateway.server.mvc.filter; +import java.net.URI; import java.util.Collections; import org.assertj.core.api.Assertions; @@ -28,6 +29,7 @@ import org.springframework.boot.tomcat.autoconfigure.servlet.TomcatServletWebServerAutoConfiguration; import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration; import org.springframework.cloud.gateway.server.mvc.GatewayServerMvcAutoConfiguration; +import org.springframework.cloud.gateway.server.mvc.common.MvcUtils; import org.springframework.cloud.gateway.server.mvc.config.GatewayMvcProperties; import org.springframework.cloud.gateway.server.mvc.predicate.PredicateAutoConfiguration; import org.springframework.http.HttpHeaders; @@ -39,6 +41,7 @@ import static org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter.X_FORWARDED_FOR_HEADER; import static org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter.X_FORWARDED_HOST_HEADER; import static org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter.X_FORWARDED_PORT_HEADER; +import static org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter.X_FORWARDED_PREFIX_HEADER; import static org.springframework.cloud.gateway.server.mvc.filter.XForwardedRequestHeadersFilter.X_FORWARDED_PROTO_HEADER; /** @@ -162,4 +165,44 @@ public void remoteAdddressIsNullUnTrustedProxyNotAppended() { assertThat(headers.getFirst(X_FORWARDED_FOR_HEADER)).doesNotContain("127.0.0.1"); } + @Test + public void prefixToInferOnceWhenChainedPathFiltersProcessRequest() { + MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost:8080/blue") + .remoteAddress("10.0.0.1:80") + .buildRequest(null); + ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList()); + + // two chained path filters, e.g. StripPrefix=1 twice, add one entry each + MvcUtils.addOriginalRequestUrl(request, URI.create("http://localhost:8080/tenant/api/blue")); + MvcUtils.addOriginalRequestUrl(request, URI.create("http://localhost:8080/api/blue")); + + XForwardedRequestHeadersFilter filter = new XForwardedRequestHeadersFilter( + new XForwardedRequestHeadersFilterProperties(), ".*"); + + HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request); + + assertThat(headers.headerNames()).contains(X_FORWARDED_PREFIX_HEADER); + + assertThat(headers.getFirst(X_FORWARDED_PREFIX_HEADER)).isEqualTo("/tenant/api"); + } + + @Test + public void prefixAppendedToExistingHeaderOnceWhenChainedPathFiltersProcessRequest() { + MockHttpServletRequest servletRequest = MockMvcRequestBuilders.get("http://localhost:8080/blue") + .remoteAddress("10.0.0.1:80") + .header(X_FORWARDED_PREFIX_HEADER, "/upstream") + .buildRequest(null); + ServerRequest request = ServerRequest.create(servletRequest, Collections.emptyList()); + + MvcUtils.addOriginalRequestUrl(request, URI.create("http://localhost:8080/tenant/api/blue")); + MvcUtils.addOriginalRequestUrl(request, URI.create("http://localhost:8080/api/blue")); + + XForwardedRequestHeadersFilter filter = new XForwardedRequestHeadersFilter( + new XForwardedRequestHeadersFilterProperties(), ".*"); + + HttpHeaders headers = filter.apply(request.headers().asHttpHeaders(), request); + + assertThat(headers.getFirst(X_FORWARDED_PREFIX_HEADER)).isEqualTo("/upstream,/tenant/api"); + } + }