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 3bda4a24d..37c5f2649 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 @@ -261,22 +261,23 @@ public HttpHeaders filter(HttpHeaders input, ServerWebExchange exchange) { LinkedHashSet originalUris = exchange.getAttribute(GATEWAY_ORIGINAL_REQUEST_URL_ATTR); URI requestUri = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); - 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-4236) + URI originalUri = originalUris.iterator().next(); - if (originalUri != null && originalUri.getPath() != null) { - String prefix = originalUri.getPath(); + 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); + // 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-webflux/src/test/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilterTests.java b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilterTests.java index 5749d951f..2761ab0aa 100644 --- a/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilterTests.java +++ b/spring-cloud-gateway-server-webflux/src/test/java/org/springframework/cloud/gateway/filter/headers/XForwardedHeadersFilterTests.java @@ -189,6 +189,58 @@ public void prefixToInfer() throws Exception { assertThat(headers.getFirst(X_FORWARDED_PREFIX_HEADER)).isEqualTo("/prefix"); } + @Test + public void prefixToInferOnceWhenChainedPathFiltersProcessRequest() throws Exception { + MockServerHttpRequest request = MockServerHttpRequest.get("https://originalhost:8080/tenant/api/blue") + .remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80)) + .build(); + + XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX); + filter.setPrefixAppend(true); + filter.setPrefixEnabled(true); + + ServerWebExchange exchange = MockServerWebExchange.from(request); + LinkedHashSet originalUris = new LinkedHashSet<>(); + // two chained path filters, e.g. StripPrefix=1 twice, add one entry each + originalUris + .add(UriComponentsBuilder.fromUriString("https://originalhost:8080/tenant/api/blue").build().toUri()); + originalUris.add(UriComponentsBuilder.fromUriString("https://originalhost:8080/api/blue").build().toUri()); + exchange.getAttributes().put(GATEWAY_ORIGINAL_REQUEST_URL_ATTR, originalUris); + URI requestUri = UriComponentsBuilder.fromUriString("https://routedservice:8090/blue").build().toUri(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUri); + + HttpHeaders headers = filter.filter(request.getHeaders(), exchange); + + assertThat(headers.headerNames()).contains(X_FORWARDED_PREFIX_HEADER); + + assertThat(headers.getFirst(X_FORWARDED_PREFIX_HEADER)).isEqualTo("/tenant/api"); + } + + @Test + public void prefixAppendedToExistingHeaderOnceWhenChainedPathFiltersProcessRequest() throws Exception { + MockServerHttpRequest request = MockServerHttpRequest.get("https://originalhost:8080/tenant/api/blue") + .remoteAddress(new InetSocketAddress(InetAddress.getByName("10.0.0.1"), 80)) + .header(X_FORWARDED_PREFIX_HEADER, "/upstream") + .build(); + + XForwardedHeadersFilter filter = new XForwardedHeadersFilter(ALLOW_ALL_REGEX); + filter.setPrefixAppend(true); + filter.setPrefixEnabled(true); + + ServerWebExchange exchange = MockServerWebExchange.from(request); + LinkedHashSet originalUris = new LinkedHashSet<>(); + originalUris + .add(UriComponentsBuilder.fromUriString("https://originalhost:8080/tenant/api/blue").build().toUri()); + originalUris.add(UriComponentsBuilder.fromUriString("https://originalhost:8080/api/blue").build().toUri()); + exchange.getAttributes().put(GATEWAY_ORIGINAL_REQUEST_URL_ATTR, originalUris); + URI requestUri = UriComponentsBuilder.fromUriString("https://routedservice:8090/blue").build().toUri(); + exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUri); + + HttpHeaders headers = filter.filter(request.getHeaders(), exchange); + + assertThat(headers.getFirst(X_FORWARDED_PREFIX_HEADER)).isEqualTo("/upstream,/tenant/api"); + } + @Test public void prefixToInferWhenEqualsResource() throws Exception { MockServerHttpRequest request = MockServerHttpRequest.get("https://originalhost:8080/resource/resource/")