Skip to content

Commit

Permalink
Make function to extract client IP configurable. Add additional metho…
Browse files Browse the repository at this point in the history
…d for extracting client IP with is safer from spoofing of X-Forwarded-For header.
  • Loading branch information
fitzoh committed Feb 14, 2018
1 parent 14b1e21 commit 6ff0fd6
Show file tree
Hide file tree
Showing 3 changed files with 233 additions and 27 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public Predicate<ServerWebExchange> apply(Tuple args) {
addSource(sources, (String) arg);
}
}
return apply(sources, false);
return apply(sources);
}

public Predicate<ServerWebExchange> apply(String... addrs) {
Expand All @@ -60,21 +60,24 @@ public Predicate<ServerWebExchange> apply(String... addrs) {
for (String addr : addrs) {
addSource(sources, addr);
}
return apply(sources, false);
return apply(sources);
}


public Predicate<ServerWebExchange> apply(List<IpSubnetFilterRule> sources){
return apply(sources, RoutePredicateFactoryUtils::extractDefaultRemoteAddress);
}

/**
* @param respectForwardedHeader whether to check the `X-Forwarded-For` header for the
* remote IP address.
*
* @param remoteAddressResolver the function to use to determine the remote address of the request.
*
*/
public Predicate<ServerWebExchange> apply(List<IpSubnetFilterRule> sources,
boolean respectForwardedHeader) {
Function<ServerWebExchange, InetSocketAddress> remoteIpResolver = respectForwardedHeader
? RoutePredicateFactoryUtils::parseRemoteIpRespectingForwardedHeader
: RoutePredicateFactoryUtils::parseRemoteIpIgnoringForwardedHeader;
Function<ServerWebExchange, InetSocketAddress> remoteAddressResolver) {

return exchange -> {
InetSocketAddress remoteAddress = remoteIpResolver.apply(exchange);
InetSocketAddress remoteAddress = remoteAddressResolver.apply(exchange);
if (remoteAddress != null) {

String hostAddress = remoteAddress.getAddress().getHostAddress();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,13 @@
package org.springframework.cloud.gateway.handler.support;

import java.net.InetSocketAddress;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.logging.log4j.util.Strings;
import org.springframework.cloud.gateway.handler.predicate.RoutePredicateFactory;
import org.springframework.web.server.ServerWebExchange;

Expand All @@ -44,17 +47,80 @@ public static void traceMatch(String prefix, Object desired, Object actual, bool
}


public static InetSocketAddress parseRemoteIpRespectingForwardedHeader(ServerWebExchange exchange) {
List<String> xForwardedValues = exchange.getRequest().getHeaders().get(X_FORWARDED_FOR);
if (xForwardedValues != null && xForwardedValues.size() != 0) {
String remoteAddress = xForwardedValues.get(0).split(", ")[0];
return InetSocketAddress.createUnresolved(remoteAddress, 0);
/**
* Extracts the last trusted IP address found in the X-Forwarded-For header if one is present.
* Falls back to the {@link #extractDefaultRemoteAddress(ServerWebExchange)} otherwise.
*
* This method 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.0, 0.0.0.1, 0.0.0.2]:
*
* maxTrustedIndex -> result
*
* [MIN_VALUE,0] -> IllegalArgumentException
* 1 -> 0.0.0.2
* 2 -> 0.0.0.1
* 3 -> 0.0.0.0
* [4, MAX_VALUE] -> 0.0.0.0
*
* @param maxTrustedIndex correlates to the number of trusted proxies expected in front spring cloud gateway
* (index starts at 1).
*/
public static InetSocketAddress extractRemoteAddressFromXForwardedHeaderByMaxTrustedIndex(ServerWebExchange exchange, int maxTrustedIndex) {
if (maxTrustedIndex < 1){
throw new IllegalArgumentException("An index greater than 0 is required");
}
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 parseRemoteIpIgnoringForwardedHeader(exchange);
return extractDefaultRemoteAddress(exchange);
}

public static InetSocketAddress parseRemoteIpIgnoringForwardedHeader(ServerWebExchange exchange) {
/**
* Extracts the first IP address found in the X-Forwarded-For header if one is present.
* Falls back to the {@link #extractDefaultRemoteAddress(ServerWebExchange)} otherwise.
* This method is potentially vulnerable to spoofing by tampering with the X-Forwarded-For header is tampered with.
* If the resulting IP address is used for security purposes, use {@link RoutePredicateFactoryUtils#extractDefaultRemoteAddress(ServerWebExchange)} instead.
*/
public static InetSocketAddress extractFinalRemoteAddressFromXForwardedHeader(ServerWebExchange exchange) {
return extractRemoteAddressFromXForwardedHeaderByMaxTrustedIndex(exchange, Integer.MAX_VALUE);
}

/**
* Ignore any X-Forwarded-For header and return the remote address from the exchange
*/
public static InetSocketAddress extractDefaultRemoteAddress(ServerWebExchange exchange) {
return exchange.getRequest().getRemoteAddress();
}

/**
* 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.
*/
public static List<String> extractXForwardedValues(ServerWebExchange exchange){
List<String> xForwardedValues = exchange.getRequest().getHeaders().get(X_FORWARDED_FOR);
if (xForwardedValues == null || xForwardedValues.size() != 1){
if ( xForwardedValues != null && xForwardedValues.size() > 0){
logger.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;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@

import static org.assertj.core.api.Assertions.assertThat;

import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.List;

import org.junit.Test;
import org.springframework.mock.http.server.reactive.MockServerHttpRequest;
Expand All @@ -14,36 +13,174 @@ public class RoutePredicateFactoryUtilsTest {

private final InetSocketAddress remote0000Addres = InetSocketAddress.createUnresolved("0.0.0.0", 1234);

//RoutePredicateFactoryUtils::extractRemoteAddressFromXForwardedHeaderByMaxTrustedIndex

@Test
public void extractTrustedPrioritizesFirstForwardedIp() {
MockServerHttpRequest request = MockServerHttpRequest.get("someUrl")
.remoteAddress(remote0000Addres)
.header("X-Forwarded-For", "0.0.0.1, 0.0.0.2, 0.0.0.3")
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);

InetSocketAddress actualIp = RoutePredicateFactoryUtils
.extractRemoteAddressFromXForwardedHeaderByMaxTrustedIndex(exchange, 1);

assertThat(actualIp.getHostName()).isEqualTo("0.0.0.3");
}

@Test
public void parseRemoteIpPrioritizesFirstForwardedIp() {
public void extractTrustedFallsBackToRemoteIp() {
MockServerHttpRequest request = MockServerHttpRequest.get("someUrl")
.remoteAddress(remote0000Addres)
.header("X-Forwarded-For", "0.0.0.1, 0.0.0.2, 0.0.0.3").build();
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);

InetSocketAddress actualIp = RoutePredicateFactoryUtils
.parseRemoteIpRespectingForwardedHeader(exchange);
.extractRemoteAddressFromXForwardedHeaderByMaxTrustedIndex(exchange, 1);

assertThat(actualIp.getHostName()).isEqualTo("0.0.0.0");
}

@Test
public void extractTrustedReturnsNullIfNoForwardedOrRemoteIp() {
MockServerHttpRequest request = MockServerHttpRequest.get("someUrl")
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);

InetSocketAddress actualIp = RoutePredicateFactoryUtils
.extractRemoteAddressFromXForwardedHeaderByMaxTrustedIndex(exchange, 1);

assertThat(actualIp).isEqualTo(null);
}

//RoutePredicateFactoryUtils::extractFinalRemoteAddressFromXForwardedHeader

@Test
public void extractFinalPrioritizesFirstForwardedIp() {
MockServerHttpRequest request = MockServerHttpRequest.get("someUrl")
.remoteAddress(remote0000Addres)
.header("X-Forwarded-For", "0.0.0.1, 0.0.0.2, 0.0.0.3")
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);

InetSocketAddress actualIp = RoutePredicateFactoryUtils
.extractFinalRemoteAddressFromXForwardedHeader(exchange);

assertThat(actualIp.getHostName()).isEqualTo("0.0.0.1");
}

@Test
public void parseRemoteIpFallsBackToRemoteIp() {
public void extractFinalFallsBackToRemoteIp() {
MockServerHttpRequest request = MockServerHttpRequest.get("someUrl")
.remoteAddress(remote0000Addres).build();
.remoteAddress(remote0000Addres)
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);

InetSocketAddress actualIp = RoutePredicateFactoryUtils
.parseRemoteIpRespectingForwardedHeader(exchange);
.extractFinalRemoteAddressFromXForwardedHeader(exchange);

assertThat(actualIp.getHostName()).isEqualTo("0.0.0.0");
}

@Test
public void parseRemoteIpReturnsNullIfNoForwardedOrRemoteIp() {
MockServerHttpRequest request = MockServerHttpRequest.get("someUrl").build();
public void extractFinalReturnsNullIfNoForwardedOrRemoteIp() {
MockServerHttpRequest request = MockServerHttpRequest.get("someUrl")
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);

InetSocketAddress actualIp = RoutePredicateFactoryUtils
.parseRemoteIpRespectingForwardedHeader(exchange);
.extractFinalRemoteAddressFromXForwardedHeader(exchange);

assertThat(actualIp).isEqualTo(null);
}

//RoutePredicateFactoryUtils::extractDefaultRemoteAddress(exchange);

@Test
public void extractDefaultRemoteAddressHappyPath() {
MockServerHttpRequest request = MockServerHttpRequest.get("someUrl")
.remoteAddress(remote0000Addres)
.header("X-Forwarded-For", "0.0.0.1, 0.0.0.2, 0.0.0.3")
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);

InetSocketAddress actualIp = RoutePredicateFactoryUtils.extractDefaultRemoteAddress(exchange);

assertThat(actualIp.getHostName()).isEqualTo("0.0.0.0");
}

@Test
public void extractDefaultRemoteAddressNoRemote() {
MockServerHttpRequest request = MockServerHttpRequest.get("someUrl")
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);

InetSocketAddress actualIp = RoutePredicateFactoryUtils.extractDefaultRemoteAddress(exchange);

assertThat(actualIp).isNull();
}

// RoutePredicateFactoryUtils::extractXForwardedValues

@Test
public void extractXForwardedHappyPath() {
MockServerHttpRequest request = MockServerHttpRequest.get("someUrl")
.header("X-Forwarded-For", "0.0.0.1, 0.0.0.2, 0.0.0.3")
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);

List<String> values = RoutePredicateFactoryUtils.extractXForwardedValues(exchange);

assertThat(values).containsExactly("0.0.0.1", "0.0.0.2", "0.0.0.3");
}

@Test
public void extractXForwardedSingleValue() {
MockServerHttpRequest request = MockServerHttpRequest.get("someUrl")
.header("X-Forwarded-For", "0.0.0.1")
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);

List<String> values = RoutePredicateFactoryUtils.extractXForwardedValues(exchange);

assertThat(values).containsExactly("0.0.0.1");
}


@Test
public void extractXForwardedEmpty() {
MockServerHttpRequest request = MockServerHttpRequest.get("someUrl")
.header("X-Forwarded-For", "")
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);

List<String> values = RoutePredicateFactoryUtils.extractXForwardedValues(exchange);

assertThat(values).hasSize(0);
}

@Test
public void extractXForwardedNoHeader() {
MockServerHttpRequest request = MockServerHttpRequest.get("someUrl")
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);

List<String> values = RoutePredicateFactoryUtils.extractXForwardedValues(exchange);

assertThat(values).hasSize(0);
}

@Test
public void extractXForwardedMultipleHeaders() {
MockServerHttpRequest request = MockServerHttpRequest.get("someUrl")
.header("X-Forwarded-For", "0.0.0.1")
.header("X-Forwarded-For", "0.0.0.2")
.build();
MockServerWebExchange exchange = MockServerWebExchange.from(request);

List<String> values = RoutePredicateFactoryUtils.extractXForwardedValues(exchange);

assertThat(values).hasSize(0);
}
}

0 comments on commit 6ff0fd6

Please sign in to comment.