Add route predicate for "X-Forwarded-For" header

Fixes gh-783
Fixes gh-2384
This commit is contained in:
Jelle Druyts
2021-09-30 07:13:30 +00:00
committed by spencergibb
parent d5a9a21a5e
commit 8b685b18ca
7 changed files with 272 additions and 3 deletions

View File

@@ -134,6 +134,7 @@ import org.springframework.cloud.gateway.handler.predicate.ReadBodyRoutePredicat
import org.springframework.cloud.gateway.handler.predicate.RemoteAddrRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.WeightRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.XForwardedRemoteAddrRoutePredicateFactory;
import org.springframework.cloud.gateway.route.CachingRouteLocator;
import org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator;
import org.springframework.cloud.gateway.route.CompositeRouteLocator;
@@ -432,6 +433,12 @@ public class GatewayAutoConfiguration {
return new RemoteAddrRoutePredicateFactory();
}
@Bean
@ConditionalOnEnabledPredicate
public XForwardedRemoteAddrRoutePredicateFactory xForwardedRemoteAddrRoutePredicateFactory() {
return new XForwardedRemoteAddrRoutePredicateFactory();
}
@Bean
@DependsOn("weightCalculatorWebFilter")
@ConditionalOnEnabledPredicate

View File

@@ -0,0 +1,148 @@
/*
* Copyright 2013-2020 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.handler.predicate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.cloud.gateway.support.ipresolver.XForwardedRemoteAddressResolver;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
/*
This route predicate allows requests to be filtered based on the "X-Forwarded-For" HTTP header.
This can be used with reverse proxies such as load balancers or web application firewalls where
the request should only be allowed if it comes from a trusted list of IP addresses used by those
reverse proxies.
With this implementation, a separate "XForwardedRemoteAddr" route predicate is offered which can
be configured with a list of allowed IP addresses and which by default has "maxTrustedIndex" set to 1.
This value means we trust the last (right-most) value in the "X-Forwarded-For" header, which represents
the last reverse proxy that was used when calling the gateway. That IP address is then checked against
the list of allowed IP addresses and used to determine whether or not the request is allowed.
See https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#modifying-the-way-remote-addresses-are-resolved.
Note that this predicate implementation does not implement any core logic itself, it aggregates the
"RemoteAddrRoutePredicateFactory" and "XForwardedRemoteAddressResolver" classes into a single predicate
that can be enabled directly from application configuration without the need to specify anything in
custom code.
Example usage in application.yml which trusts two reverse proxies (one using an IPv6 range):
...
- predicates:
- XForwardedRemoteAddr="20.103.252.85", "2a01:111:2050::/44"
*/
/**
* @author Jelle Druyts
*/
public class XForwardedRemoteAddrRoutePredicateFactory
extends AbstractRoutePredicateFactory<XForwardedRemoteAddrRoutePredicateFactory.Config> {
private static final Log log = LogFactory.getLog(XForwardedRemoteAddrRoutePredicateFactory.class);
public XForwardedRemoteAddrRoutePredicateFactory() {
super(Config.class);
}
@Override
public ShortcutType shortcutType() {
return ShortcutType.GATHER_LIST;
}
@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("sources");
}
@Override
public Predicate<ServerWebExchange> apply(Config config) {
if (log.isDebugEnabled()) {
log.debug("Applying XForwardedRemoteAddr route predicate with maxTrustedIndex of "
+ config.getMaxTrustedIndex() + " for " + config.getSources().size() + " source(s)");
}
// Reuse the standard RemoteAddrRoutePredicateFactory but instead of using the
// default RemoteAddressResolver to determine the client IP address, use an
// XForwardedRemoteAddressResolver.
RemoteAddrRoutePredicateFactory.Config wrappedConfig = new RemoteAddrRoutePredicateFactory.Config();
wrappedConfig.setSources(config.getSources());
wrappedConfig
.setRemoteAddressResolver(XForwardedRemoteAddressResolver.maxTrustedIndex(config.getMaxTrustedIndex()));
RemoteAddrRoutePredicateFactory remoteAddrRoutePredicateFactory = new RemoteAddrRoutePredicateFactory();
Predicate<ServerWebExchange> wrappedPredicate = remoteAddrRoutePredicateFactory.apply(wrappedConfig);
return exchange -> {
Boolean isAllowed = wrappedPredicate.test(exchange);
if (log.isDebugEnabled()) {
ServerHttpRequest request = exchange.getRequest();
log.debug("Request for \"" + request.getURI() + "\" from client \""
+ request.getRemoteAddress().getAddress().getHostAddress() + "\" with \""
+ XForwardedRemoteAddressResolver.X_FORWARDED_FOR + "\" header value of \""
+ request.getHeaders().get(XForwardedRemoteAddressResolver.X_FORWARDED_FOR) + "\" is "
+ (isAllowed ? "ALLOWED" : "NOT ALLOWED"));
}
return isAllowed;
};
}
public static class Config {
// Trust the last (right-most) value in the "X-Forwarded-For" header by default,
// which represents the last reverse proxy that was used when calling the gateway.
private int maxTrustedIndex = 1;
private List<String> sources = new ArrayList<>();
public int getMaxTrustedIndex() {
return this.maxTrustedIndex;
}
public Config setMaxTrustedIndex(int maxTrustedIndex) {
this.maxTrustedIndex = maxTrustedIndex;
return this;
}
public List<String> getSources() {
return this.sources;
}
public Config setSources(List<String> sources) {
this.sources = sources;
return this;
}
public Config setSources(String... sources) {
this.sources = Arrays.asList(sources);
return this;
}
}
}

View File

@@ -35,6 +35,7 @@ import org.springframework.cloud.gateway.handler.predicate.QueryRoutePredicateFa
import org.springframework.cloud.gateway.handler.predicate.ReadBodyRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.RemoteAddrRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.WeightRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.XForwardedRemoteAddrRoutePredicateFactory;
import org.springframework.cloud.gateway.route.Route;
import org.springframework.cloud.gateway.support.ipresolver.RemoteAddressResolver;
import org.springframework.http.HttpMethod;
@@ -262,6 +263,22 @@ public class PredicateSpec extends UriSpec {
}));
}
/**
* A predicate which checks the remote address of the request based off of the
* {@code X-Forwarded-For} header. Useful if Spring Cloud Gateway site behind a proxy
* layer. See
* {@link org.springframework.cloud.gateway.support.ipresolver.XForwardedRemoteAddressResolver}
* for more information.
* @param addrs the remote address to verify. Should use CIDR-notation (IPv4 or IPv6)
* strings.
* @return a {@link BooleanSpec} to be used to add logical operators
*/
public BooleanSpec xForwardedRemoteAddr(String... addrs) {
return asyncPredicate(getBean(XForwardedRemoteAddrRoutePredicateFactory.class).applyAsync(c -> {
c.setSources(addrs);
}));
}
/**
* A predicate which will select a route based on its assigned weight.
* @param group the group the route belongs to

View File

@@ -73,13 +73,13 @@ public final class NameUtils {
StringBuffer stringBuffer = new StringBuffer();
while (matcher.find()) {
if (stringBuffer.length() != 0) {
matcher.appendReplacement(stringBuffer, "-" + matcher.group(1).toLowerCase());
matcher.appendReplacement(stringBuffer, "-" + matcher.group(1));
}
else {
matcher.appendReplacement(stringBuffer, matcher.group(1).toLowerCase());
matcher.appendReplacement(stringBuffer, matcher.group(1));
}
}
return stringBuffer.toString();
return stringBuffer.toString().toLowerCase();
}
private static String removeGarbage(String s) {

View File

@@ -324,6 +324,12 @@
"description": "Enables the remote-addr predicate.",
"defaultValue": "true"
},
{
"name": "spring.cloud.gateway.predicate.xforwarded-remote-addr.enabled",
"type": "java.lang.Boolean",
"description": "Enables the xforwarded-remote-addr predicate.",
"defaultValue": "true"
},
{
"name": "spring.cloud.gateway.predicate.weight.enabled",
"type": "java.lang.Boolean",

View File

@@ -85,6 +85,7 @@ public class DisableBuiltInPredicatesTests {
"spring.cloud.gateway.predicate.query.enabled=false",
"spring.cloud.gateway.predicate.read-body.enabled=false",
"spring.cloud.gateway.predicate.remote-addr.enabled=false",
"spring.cloud.gateway.predicate.xforwarded-remote-addr.enabled=false",
"spring.cloud.gateway.predicate.weight.enabled=false",
"spring.cloud.gateway.predicate.cloud-foundry-route-service.enabled=false" })
@ActiveProfiles("disable-components")

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2013-2020 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.handler.predicate;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import reactor.test.StepVerifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.cloud.gateway.test.BaseWebClientTests;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.reactive.function.client.ClientResponse;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import static org.springframework.cloud.gateway.test.TestUtils.assertStatus;
@SpringBootTest(webEnvironment = RANDOM_PORT)
@DirtiesContext
@ActiveProfiles({ "remote-address" })
public class XForwardedRemoteAddrRoutePredicateFactoryTests extends BaseWebClientTests {
@Test
public void xForwardedRemoteAddrWorks() {
Mono<ClientResponse> result = webClient.get().uri("/xforwardfor").header("X-Forwarded-For", "12.34.56.78")
.exchangeToMono(Mono::just);
StepVerifier.create(result).consumeNextWith(response -> assertStatus(response, HttpStatus.OK)).expectComplete()
.verify(Duration.ofSeconds(20));
}
@Test
public void xForwardedRemoteAddrWorksUsingRightMostValueByDefault() {
Mono<ClientResponse> result = webClient.get().uri("/xforwardfor")
.header("X-Forwarded-For", "99.99.99.99, 12.34.56.78").exchangeToMono(Mono::just);
StepVerifier.create(result).consumeNextWith(response -> assertStatus(response, HttpStatus.OK)).expectComplete()
.verify(Duration.ofSeconds(20));
}
@Test
public void xForwardedRemoteAddrRejects() {
Mono<ClientResponse> result = webClient.get().uri("/xforwardfor").header("X-Forwarded-For", "99.99.99.99")
.exchangeToMono(Mono::just);
StepVerifier.create(result).consumeNextWith(response -> assertStatus(response, HttpStatus.NOT_FOUND))
.expectComplete().verify(Duration.ofSeconds(20));
}
@EnableAutoConfiguration
@SpringBootConfiguration
@Import(DefaultTestConfig.class)
public static class TestConfig {
@Value("${test.uri}")
String uri;
@Bean
public RouteLocator testRouteLocator(RouteLocatorBuilder builder) {
return builder.routes().route("x_forwarded_for_test", r -> r.path("/xforwardfor").and()
.xForwardedRemoteAddr("12.34.56.78").filters(f -> f.setStatus(200)).uri(uri)).build();
}
}
}