Browse Source
* Update `RemoteAddrRoutePredicateFactory` to optionally respect the `X-Forwarded-For` header. Fixes gh-155. * Make function to extract client IP configurable. Add additional method for extracting client IP with is safer from spoofing of X-Forwarded-For header.pull/241/head
Andrew Fitzgerald
7 years ago
committed by
Spencer Gibb
5 changed files with 296 additions and 2 deletions
@ -0,0 +1,16 @@
@@ -0,0 +1,16 @@
|
||||
package org.springframework.cloud.gateway.support.ipresolver; |
||||
|
||||
import java.net.InetSocketAddress; |
||||
|
||||
import org.springframework.web.server.ServerWebExchange; |
||||
|
||||
/** |
||||
* @author Andrew Fitzgerald |
||||
*/ |
||||
public class DefaultRemoteAddressResolver implements RemoteAddressResolver { |
||||
|
||||
@Override |
||||
public InetSocketAddress resolve(ServerWebExchange exchange) { |
||||
return exchange.getRequest().getRemoteAddress(); |
||||
} |
||||
} |
@ -0,0 +1,13 @@
@@ -0,0 +1,13 @@
|
||||
package org.springframework.cloud.gateway.support.ipresolver; |
||||
|
||||
import java.net.InetSocketAddress; |
||||
|
||||
import org.springframework.web.server.ServerWebExchange; |
||||
|
||||
/** |
||||
* @author Andrew Fitzgerald |
||||
*/ |
||||
public interface RemoteAddressResolver { |
||||
|
||||
InetSocketAddress resolve(ServerWebExchange exchange); |
||||
} |
@ -0,0 +1,117 @@
@@ -0,0 +1,117 @@
|
||||
package org.springframework.cloud.gateway.support.ipresolver; |
||||
|
||||
import java.net.InetSocketAddress; |
||||
import java.util.Arrays; |
||||
import java.util.Collections; |
||||
import java.util.List; |
||||
|
||||
import org.apache.logging.log4j.util.Strings; |
||||
import org.slf4j.Logger; |
||||
import org.slf4j.LoggerFactory; |
||||
import org.springframework.http.server.reactive.ServerHttpRequest; |
||||
import org.springframework.util.Assert; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
|
||||
/** |
||||
* Parses the client address from the X-Forwarded-For header. If header is not present, |
||||
* falls back to {@link DefaultRemoteAddressResolver} and |
||||
* {@link ServerHttpRequest#getRemoteAddress()}. Use the static constructor methods which |
||||
* meets your security requirements. |
||||
* |
||||
* @see <a href= |
||||
* "https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For">X-Forwarded-For |
||||
* reference</a> |
||||
* @author Andrew Fitzgerald |
||||
*/ |
||||
public class XForwardedRemoteAddressResolver implements RemoteAddressResolver { |
||||
|
||||
public static final String X_FORWARDED_FOR = "X-Forwarded-For"; |
||||
private static final Logger log = LoggerFactory |
||||
.getLogger(XForwardedRemoteAddressResolver.class); |
||||
private final DefaultRemoteAddressResolver defaultRemoteIpResolver = new DefaultRemoteAddressResolver(); |
||||
|
||||
private final int maxTrustedIndex; |
||||
|
||||
private XForwardedRemoteAddressResolver(int maxTrustedIndex) { |
||||
this.maxTrustedIndex = maxTrustedIndex; |
||||
} |
||||
|
||||
/** |
||||
* @return a {@link XForwardedRemoteAddressResolver} which always extracts the first |
||||
* IP address found in the X-Forwarded-For header (when present). Equivalent to |
||||
* calling {@link #maxTrustedIndexXForwardedRemoteAddressResolver(int)} with a |
||||
* {@link #maxTrustedIndex} of {@link Integer#MAX_VALUE}. This configuration is |
||||
* vulnerable to spoofing via manually setting the X-Forwarded-For header. If the |
||||
* resulting IP address is used for security purposes, use |
||||
* {@link #maxTrustedIndexXForwardedRemoteAddressResolver(int)} instead. |
||||
*/ |
||||
public static XForwardedRemoteAddressResolver trustAllXForwardedRemoteAddressResolver() { |
||||
return new XForwardedRemoteAddressResolver(Integer.MAX_VALUE); |
||||
} |
||||
|
||||
/** |
||||
* @return a {@link XForwardedRemoteAddressResolver} which extracts the last |
||||
* <em>trusted</em> IP address found in the X-Forwarded-For header (when present). |
||||
* This configuration exists to prevent a malicious actor from spoofing the value of |
||||
* the X-Forwarded-For header. If you know that your gateway application is only |
||||
* accessible from a a trusted load balancer, then you can trust that the load |
||||
* balancer will append a valid client IP address to the X-Forwarded-For header, and |
||||
* should use a value of `1` for the `maxTrustedIndex`. |
||||
* |
||||
* |
||||
* Given the X-Forwarded-For value of [0.0.0.1, 0.0.0.2, 0.0.0.3]: |
||||
* |
||||
* <pre> |
||||
* maxTrustedIndex -> result |
||||
* |
||||
* [MIN_VALUE,0] -> IllegalArgumentException |
||||
* 1 -> 0.0.0.3 |
||||
* 2 -> 0.0.0.2 |
||||
* 3 -> 0.0.0.1 |
||||
* [4, MAX_VALUE] -> 0.0.0.1 |
||||
* </pre> |
||||
* |
||||
* @param maxTrustedIndex correlates to the number of trusted proxies expected in |
||||
* front of Spring Cloud Gateway (index starts at 1). |
||||
*/ |
||||
public static XForwardedRemoteAddressResolver maxTrustedIndexXForwardedRemoteAddressResolver( |
||||
int maxTrustedIndex) { |
||||
Assert.isTrue(maxTrustedIndex > 0, "An index greater than 0 is required"); |
||||
return new XForwardedRemoteAddressResolver(maxTrustedIndex); |
||||
} |
||||
|
||||
/** |
||||
* The X-Forwarded-For header contains a comma separated list of IP addresses. This |
||||
* method parses those IP addresses into a list. If no X-Forwarded-For header is |
||||
* found, an empty list is returned. If multiple X-Forwarded-For headers are found, an |
||||
* empty list is returned out of caution. |
||||
* @return The parsed values of the X-Forwarded-Header. |
||||
*/ |
||||
@Override |
||||
public InetSocketAddress resolve(ServerWebExchange exchange) { |
||||
List<String> xForwardedValues = extractXForwardedValues(exchange); |
||||
Collections.reverse(xForwardedValues); |
||||
if (xForwardedValues.size() != 0) { |
||||
int index = Math.min(xForwardedValues.size(), maxTrustedIndex) - 1; |
||||
return InetSocketAddress.createUnresolved(xForwardedValues.get(index), 0); |
||||
} |
||||
return defaultRemoteIpResolver.resolve(exchange); |
||||
} |
||||
|
||||
private List<String> extractXForwardedValues(ServerWebExchange exchange) { |
||||
List<String> xForwardedValues = exchange.getRequest().getHeaders() |
||||
.get(X_FORWARDED_FOR); |
||||
if (xForwardedValues == null || xForwardedValues.size() == 0) { |
||||
return Collections.emptyList(); |
||||
} |
||||
if (xForwardedValues.size() > 1) { |
||||
log.warn("Multiple X-Forwarded-For headers found, discarding all"); |
||||
return Collections.emptyList(); |
||||
} |
||||
List<String> values = Arrays.asList(xForwardedValues.get(0).split(", ")); |
||||
if (values.size() == 1 && Strings.isBlank(values.get(0))) { |
||||
return Collections.emptyList(); |
||||
} |
||||
return values; |
||||
} |
||||
} |
@ -0,0 +1,138 @@
@@ -0,0 +1,138 @@
|
||||
package org.springframework.cloud.gateway.support.ipresolver; |
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat; |
||||
|
||||
import java.net.InetSocketAddress; |
||||
|
||||
import org.junit.Test; |
||||
import org.springframework.mock.http.server.reactive.MockServerHttpRequest; |
||||
import org.springframework.mock.web.server.MockServerWebExchange; |
||||
import org.springframework.web.server.ServerWebExchange; |
||||
|
||||
public class XForwardedRemoteAddressResolverTest { |
||||
|
||||
private final InetSocketAddress remote0000Address = InetSocketAddress |
||||
.createUnresolved("0.0.0.0", 1234); |
||||
|
||||
private final XForwardedRemoteAddressResolver trustOne = XForwardedRemoteAddressResolver |
||||
.maxTrustedIndexXForwardedRemoteAddressResolver(1); |
||||
|
||||
private final XForwardedRemoteAddressResolver trustAll = XForwardedRemoteAddressResolver |
||||
.trustAllXForwardedRemoteAddressResolver(); |
||||
|
||||
@Test |
||||
public void maxIndexOneReturnsLastForwardedIp() { |
||||
ServerWebExchange exchange = buildExchange(oneTwoThreeBuilder()); |
||||
|
||||
InetSocketAddress address = trustOne.resolve(exchange); |
||||
|
||||
assertThat(address.getHostName()).isEqualTo("0.0.0.3"); |
||||
} |
||||
|
||||
@Test |
||||
public void maxIndexOneFallsBackToRemoteIp() { |
||||
ServerWebExchange exchange = buildExchange(remoteAddressOnlyBuilder()); |
||||
|
||||
InetSocketAddress address = trustOne.resolve(exchange); |
||||
|
||||
assertThat(address.getHostName()).isEqualTo("0.0.0.0"); |
||||
} |
||||
|
||||
@Test |
||||
public void maxIndexOneReturnsNullIfNoForwardedOrRemoteIp() { |
||||
ServerWebExchange exchange = buildExchange(emptyBuilder()); |
||||
|
||||
InetSocketAddress address = trustOne.resolve(exchange); |
||||
|
||||
assertThat(address).isEqualTo(null); |
||||
} |
||||
|
||||
@Test |
||||
public void trustOneFallsBackOnEmptyHeader() { |
||||
ServerWebExchange exchange = buildExchange( |
||||
remoteAddressOnlyBuilder().header("X-Forwarded-For", "")); |
||||
|
||||
InetSocketAddress address = trustOne.resolve(exchange); |
||||
|
||||
assertThat(address.getHostName()).isEqualTo("0.0.0.0"); |
||||
|
||||
} |
||||
|
||||
@Test |
||||
public void trustOneFallsBackOnMultipleHeaders() { |
||||
ServerWebExchange exchange = buildExchange( |
||||
remoteAddressOnlyBuilder().header("X-Forwarded-For", "0.0.0.1") |
||||
.header("X-Forwarded-For", "0.0.0.2")); |
||||
|
||||
InetSocketAddress address = trustOne.resolve(exchange); |
||||
|
||||
assertThat(address.getHostName()).isEqualTo("0.0.0.0"); |
||||
} |
||||
|
||||
@Test |
||||
public void trustAllReturnsFirstForwardedIp() { |
||||
ServerWebExchange exchange = buildExchange(oneTwoThreeBuilder()); |
||||
|
||||
InetSocketAddress address = trustAll.resolve(exchange); |
||||
|
||||
assertThat(address.getHostName()).isEqualTo("0.0.0.1"); |
||||
} |
||||
|
||||
@Test |
||||
public void trustAllFinalFallsBackToRemoteIp() { |
||||
ServerWebExchange exchange = buildExchange(remoteAddressOnlyBuilder()); |
||||
|
||||
InetSocketAddress address = trustAll.resolve(exchange); |
||||
|
||||
assertThat(address.getHostName()).isEqualTo("0.0.0.0"); |
||||
} |
||||
|
||||
@Test |
||||
public void trustAllReturnsNullIfNoForwardedOrRemoteIp() { |
||||
ServerWebExchange exchange = buildExchange(emptyBuilder()); |
||||
|
||||
InetSocketAddress address = trustAll.resolve(exchange); |
||||
|
||||
assertThat(address).isEqualTo(null); |
||||
} |
||||
|
||||
@Test |
||||
public void trustAllFallsBackOnEmptyHeader() { |
||||
ServerWebExchange exchange = buildExchange( |
||||
remoteAddressOnlyBuilder().header("X-Forwarded-For", "")); |
||||
|
||||
InetSocketAddress address = trustAll.resolve(exchange); |
||||
|
||||
assertThat(address.getHostName()).isEqualTo("0.0.0.0"); |
||||
|
||||
} |
||||
|
||||
@Test |
||||
public void trustAllFallsBackOnMultipleHeaders() { |
||||
ServerWebExchange exchange = buildExchange( |
||||
remoteAddressOnlyBuilder().header("X-Forwarded-For", "0.0.0.1") |
||||
.header("X-Forwarded-For", "0.0.0.2")); |
||||
|
||||
InetSocketAddress address = trustAll.resolve(exchange); |
||||
|
||||
assertThat(address.getHostName()).isEqualTo("0.0.0.0"); |
||||
} |
||||
|
||||
private MockServerHttpRequest.BaseBuilder emptyBuilder() { |
||||
return MockServerHttpRequest.get("someUrl"); |
||||
} |
||||
|
||||
private MockServerHttpRequest.BaseBuilder remoteAddressOnlyBuilder() { |
||||
return MockServerHttpRequest.get("someUrl").remoteAddress(remote0000Address); |
||||
} |
||||
|
||||
private MockServerHttpRequest.BaseBuilder oneTwoThreeBuilder() { |
||||
return MockServerHttpRequest.get("someUrl").remoteAddress(remote0000Address) |
||||
.header("X-Forwarded-For", "0.0.0.1, 0.0.0.2, 0.0.0.3"); |
||||
} |
||||
|
||||
private ServerWebExchange buildExchange( |
||||
MockServerHttpRequest.BaseBuilder requestBuilder) { |
||||
return MockServerWebExchange.from(requestBuilder.build()); |
||||
} |
||||
} |
Loading…
Reference in new issue