Skip to content

Commit 883ad09

Browse files
Kirill Serebrennikovrstoyanchev
authored andcommitted
Add support for X-Forwarded-For and Forwarded for
See gh-23260, gh-23582
1 parent ff9daa9 commit 883ad09

File tree

7 files changed

+327
-11
lines changed

7 files changed

+327
-11
lines changed

spring-web/src/main/java/org/springframework/http/server/reactive/DefaultServerHttpRequestBuilder.java

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder {
5757
@Nullable
5858
private SslInfo sslInfo;
5959

60+
@Nullable
61+
private InetSocketAddress remoteAddress;
62+
6063
private Flux<DataBuffer> body;
6164

6265
private final ServerHttpRequest originalRequest;
@@ -68,6 +71,7 @@ public DefaultServerHttpRequestBuilder(ServerHttpRequest original) {
6871
this.uri = original.getURI();
6972
this.headers = HttpHeaders.writableHttpHeaders(original.getHeaders());
7073
this.httpMethodValue = original.getMethodValue();
74+
this.remoteAddress = original.getRemoteAddress();
7175
this.body = original.getBody();
7276
this.originalRequest = original;
7377
}
@@ -117,10 +121,16 @@ public ServerHttpRequest.Builder sslInfo(SslInfo sslInfo) {
117121
return this;
118122
}
119123

124+
@Override
125+
public ServerHttpRequest.Builder remoteAddress(InetSocketAddress remoteAddress) {
126+
this.remoteAddress = remoteAddress;
127+
return this;
128+
}
129+
120130
@Override
121131
public ServerHttpRequest build() {
122132
return new MutatedServerHttpRequest(getUriToUse(), this.contextPath,
123-
this.httpMethodValue, this.sslInfo, this.body, this.originalRequest);
133+
this.httpMethodValue, this.sslInfo, this.remoteAddress, this.body, this.originalRequest);
124134
}
125135

126136
private URI getUriToUse() {
@@ -169,18 +179,22 @@ private static class MutatedServerHttpRequest extends AbstractServerHttpRequest
169179
@Nullable
170180
private final SslInfo sslInfo;
171181

182+
@Nullable
183+
private InetSocketAddress remoteAddress;
184+
172185
private final Flux<DataBuffer> body;
173186

174187
private final ServerHttpRequest originalRequest;
175188

176189

177190
public MutatedServerHttpRequest(URI uri, @Nullable String contextPath,
178-
String methodValue, @Nullable SslInfo sslInfo,
191+
String methodValue, @Nullable SslInfo sslInfo, @Nullable InetSocketAddress remoteAddress,
179192
Flux<DataBuffer> body, ServerHttpRequest originalRequest) {
180193

181194
super(uri, contextPath, originalRequest.getHeaders());
182195
this.methodValue = methodValue;
183-
this.sslInfo = sslInfo != null ? sslInfo : originalRequest.getSslInfo();
196+
this.remoteAddress = (remoteAddress != null ? remoteAddress : originalRequest.getRemoteAddress());
197+
this.sslInfo = (sslInfo != null ? sslInfo : originalRequest.getSslInfo());
184198
this.body = body;
185199
this.originalRequest = originalRequest;
186200
}
@@ -204,7 +218,7 @@ public InetSocketAddress getLocalAddress() {
204218
@Override
205219
@Nullable
206220
public InetSocketAddress getRemoteAddress() {
207-
return this.originalRequest.getRemoteAddress();
221+
return this.remoteAddress;
208222
}
209223

210224
@Override

spring-web/src/main/java/org/springframework/http/server/reactive/ServerHttpRequest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,12 @@ interface Builder {
175175
*/
176176
Builder sslInfo(SslInfo sslInfo);
177177

178+
/**
179+
* Set the address of the remote client.
180+
* @since 5.3
181+
*/
182+
Builder remoteAddress(InetSocketAddress remoteAddress);
183+
178184
/**
179185
* Build a {@link ServerHttpRequest} decorator with the mutated properties.
180186
*/

spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Map;
2525
import java.util.Set;
2626
import java.util.function.Supplier;
27+
import java.util.regex.Pattern;
2728

2829
import javax.servlet.FilterChain;
2930
import javax.servlet.ServletException;
@@ -32,6 +33,7 @@
3233
import javax.servlet.http.HttpServletResponse;
3334
import javax.servlet.http.HttpServletResponseWrapper;
3435

36+
import org.springframework.http.HttpHeaders;
3537
import org.springframework.http.HttpRequest;
3638
import org.springframework.http.HttpStatus;
3739
import org.springframework.http.server.ServletServerHttpRequest;
@@ -67,7 +69,7 @@
6769
public class ForwardedHeaderFilter extends OncePerRequestFilter {
6870

6971
private static final Set<String> FORWARDED_HEADER_NAMES =
70-
Collections.newSetFromMap(new LinkedCaseInsensitiveMap<>(6, Locale.ENGLISH));
72+
Collections.newSetFromMap(new LinkedCaseInsensitiveMap<>(10, Locale.ENGLISH));
7173

7274
static {
7375
FORWARDED_HEADER_NAMES.add("Forwarded");
@@ -76,6 +78,7 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter {
7678
FORWARDED_HEADER_NAMES.add("X-Forwarded-Proto");
7779
FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix");
7880
FORWARDED_HEADER_NAMES.add("X-Forwarded-Ssl");
81+
FORWARDED_HEADER_NAMES.add("X-Forwarded-For");
7982
}
8083

8184

@@ -217,6 +220,10 @@ public Enumeration<String> getHeaderNames() {
217220
*/
218221
private static class ForwardedHeaderExtractingRequest extends ForwardedHeaderRemovingRequest {
219222

223+
private static final String X_FORWARDED_FOR_HEADER = "X-Forwarded-For";
224+
private static final String FORWARDED_HEADER = "Forwarded";
225+
private static final Pattern FORWARDED_FOR_PATTERN = Pattern.compile("(?i:^[^,]*for=.+)");
226+
220227
@Nullable
221228
private final String scheme;
222229

@@ -227,6 +234,14 @@ private static class ForwardedHeaderExtractingRequest extends ForwardedHeaderRem
227234

228235
private final int port;
229236

237+
@Nullable
238+
private final String remoteHost;
239+
240+
@Nullable
241+
private final String remoteAddr;
242+
243+
private final int remotePort;
244+
230245
private final ForwardedPrefixExtractor forwardedPrefixExtractor;
231246

232247

@@ -242,6 +257,25 @@ private static class ForwardedHeaderExtractingRequest extends ForwardedHeaderRem
242257
this.host = uriComponents.getHost();
243258
this.port = (port == -1 ? (this.secure ? 443 : 80) : port);
244259

260+
HttpHeaders headers = httpRequest.getHeaders();
261+
boolean hasForwardedFor = StringUtils.hasText(headers.getFirst(X_FORWARDED_FOR_HEADER)) ||
262+
(StringUtils.hasText(headers.getFirst(FORWARDED_HEADER)) &&
263+
FORWARDED_FOR_PATTERN.matcher(headers.getFirst(FORWARDED_HEADER)).matches());
264+
if (hasForwardedFor) {
265+
UriComponents remoteUriComponents = UriComponentsBuilder.newInstance()
266+
.host(request.getRemoteHost())
267+
.port(request.getRemotePort())
268+
.adaptFromForwardedForHeader(headers)
269+
.build();
270+
this.remoteHost = remoteUriComponents.getHost();
271+
this.remoteAddr = this.remoteHost;
272+
this.remotePort = remoteUriComponents.getPort();
273+
} else {
274+
this.remoteHost = request.getRemoteHost();
275+
this.remoteAddr = request.getRemoteAddr();
276+
this.remotePort = request.getRemotePort();
277+
}
278+
245279
String baseUrl = this.scheme + "://" + this.host + (port == -1 ? "" : ":" + port);
246280
Supplier<HttpServletRequest> delegateRequest = () -> (HttpServletRequest) getRequest();
247281
this.forwardedPrefixExtractor = new ForwardedPrefixExtractor(delegateRequest, pathHelper, baseUrl);
@@ -284,6 +318,23 @@ public String getRequestURI() {
284318
public StringBuffer getRequestURL() {
285319
return this.forwardedPrefixExtractor.getRequestUrl();
286320
}
321+
322+
@Override
323+
@Nullable
324+
public String getRemoteHost() {
325+
return this.remoteHost;
326+
}
327+
328+
@Override
329+
@Nullable
330+
public String getRemoteAddr() {
331+
return this.remoteAddr;
332+
}
333+
334+
@Override
335+
public int getRemotePort() {
336+
return remotePort;
337+
}
287338
}
288339

289340

spring-web/src/main/java/org/springframework/web/server/adapter/ForwardedHeaderTransformer.java

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,21 @@
1616

1717
package org.springframework.web.server.adapter;
1818

19+
import java.net.InetSocketAddress;
1920
import java.net.URI;
2021
import java.util.Collections;
2122
import java.util.Locale;
2223
import java.util.Set;
2324
import java.util.function.Function;
25+
import java.util.regex.Pattern;
2426

2527
import org.springframework.context.ApplicationContext;
2628
import org.springframework.http.HttpHeaders;
2729
import org.springframework.http.server.reactive.ServerHttpRequest;
2830
import org.springframework.lang.Nullable;
2931
import org.springframework.util.LinkedCaseInsensitiveMap;
3032
import org.springframework.util.StringUtils;
33+
import org.springframework.web.util.UriComponents;
3134
import org.springframework.web.util.UriComponentsBuilder;
3235

3336
/**
@@ -50,8 +53,11 @@
5053
*/
5154
public class ForwardedHeaderTransformer implements Function<ServerHttpRequest, ServerHttpRequest> {
5255

56+
private static final String X_FORWARDED_FOR_HEADER = "X-Forwarded-For";
57+
private static final String FORWARDED_HEADER = "Forwarded";
58+
private static final Pattern FORWARDED_FOR_PATTERN = Pattern.compile("(?i:^[^,]*for=.+)");
5359
static final Set<String> FORWARDED_HEADER_NAMES =
54-
Collections.newSetFromMap(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH));
60+
Collections.newSetFromMap(new LinkedCaseInsensitiveMap<>(10, Locale.ENGLISH));
5561

5662
static {
5763
FORWARDED_HEADER_NAMES.add("Forwarded");
@@ -60,6 +66,7 @@ public class ForwardedHeaderTransformer implements Function<ServerHttpRequest, S
6066
FORWARDED_HEADER_NAMES.add("X-Forwarded-Proto");
6167
FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix");
6268
FORWARDED_HEADER_NAMES.add("X-Forwarded-Ssl");
69+
FORWARDED_HEADER_NAMES.add("X-Forwarded-For");
6370
}
6471

6572

@@ -100,6 +107,27 @@ public ServerHttpRequest apply(ServerHttpRequest request) {
100107
builder.path(prefix + uri.getRawPath());
101108
builder.contextPath(prefix);
102109
}
110+
InetSocketAddress remoteAddress = request.getRemoteAddress();
111+
HttpHeaders headers = request.getHeaders();
112+
boolean hasForwardedFor = StringUtils.hasText(headers.getFirst(X_FORWARDED_FOR_HEADER)) ||
113+
(StringUtils.hasText(headers.getFirst(FORWARDED_HEADER)) &&
114+
FORWARDED_FOR_PATTERN.matcher(headers.getFirst(FORWARDED_HEADER)).matches());
115+
if (hasForwardedFor) {
116+
String originalRemoteHost = ((remoteAddress != null) ? remoteAddress.getHostName() : null);
117+
int originalRemotePort = ((remoteAddress != null) ? remoteAddress.getPort() : -1);
118+
UriComponents remoteUriComponents = UriComponentsBuilder.newInstance()
119+
.host(originalRemoteHost)
120+
.port(originalRemotePort)
121+
.adaptFromForwardedForHeader(headers)
122+
.build();
123+
String remoteHost = remoteUriComponents.getHost();
124+
int remotePort = (remoteUriComponents.getPort() != -1 ? remoteUriComponents.getPort() : 0);
125+
if (remoteHost != null) {
126+
builder.remoteAddress(InetSocketAddress.createUnresolved(remoteHost, remotePort));
127+
}
128+
} else {
129+
builder.remoteAddress(remoteAddress);
130+
}
103131
}
104132
removeForwardedHeaders(builder);
105133
request = builder.build();

spring-web/src/main/java/org/springframework/web/util/UriComponentsBuilder.java

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,9 +97,13 @@ public class UriComponentsBuilder implements UriBuilder, Cloneable {
9797
"^" + HTTP_PATTERN + "(//(" + USERINFO_PATTERN + "@)?" + HOST_PATTERN + "(:" + PORT_PATTERN + ")?" + ")?" +
9898
PATH_PATTERN + "(\\?" + LAST_PATTERN + ")?");
9999

100-
private static final Pattern FORWARDED_HOST_PATTERN = Pattern.compile("host=\"?([^;,\"]+)\"?");
100+
private static final Pattern FORWARDED_HOST_PATTERN = Pattern.compile("(?i:host)=\"?([^;,\"]+)\"?");
101101

102-
private static final Pattern FORWARDED_PROTO_PATTERN = Pattern.compile("proto=\"?([^;,\"]+)\"?");
102+
private static final Pattern FORWARDED_PROTO_PATTERN = Pattern.compile("(?i:proto)=\"?([^;,\"]+)\"?");
103+
104+
private static final Pattern FORWARDED_FOR_PATTERN = Pattern.compile("(?i:for)=\"?([^;,\"]+)\"?");
105+
106+
private static final String FORWARDED_FOR_NUMERIC_PORT_PATTERN = "^(\\d{1,5})$";
103107

104108
private static final Object[] EMPTY_VALUES = new Object[0];
105109

@@ -723,6 +727,33 @@ public UriComponentsBuilder uriVariables(Map<String, Object> uriVariables) {
723727
return this;
724728
}
725729

730+
/**
731+
* Adapt this builders's host+port from the "for" parameter of the "Forwarded"
732+
* header or from "X-Forwarded-For" if "Forwarded" is not found. If neither
733+
* "Forwarded" nor "X-Forwarded-For" is found no changes are made to the
734+
* builder.
735+
* @param headers the HTTP headers to consider
736+
* @return this UriComponentsBuilder
737+
*/
738+
public UriComponentsBuilder adaptFromForwardedForHeader(HttpHeaders headers) {
739+
String forwardedHeader = headers.getFirst("Forwarded");
740+
if (StringUtils.hasText(forwardedHeader)) {
741+
String forwardedToUse = StringUtils.tokenizeToStringArray(forwardedHeader, ",")[0];
742+
Matcher matcher = FORWARDED_FOR_PATTERN.matcher(forwardedToUse);
743+
if (matcher.find()) {
744+
adaptForwardedForHost(matcher.group(1).trim());
745+
}
746+
}
747+
else {
748+
String forHeader = headers.getFirst("X-Forwarded-For");
749+
if (StringUtils.hasText(forHeader)) {
750+
String forwardedForToUse = StringUtils.tokenizeToStringArray(forHeader, ",")[0];
751+
host(forwardedForToUse);
752+
}
753+
}
754+
return this;
755+
}
756+
726757
/**
727758
* Adapt this builder's scheme+host+port from the given headers, specifically
728759
* "Forwarded" (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>,
@@ -809,6 +840,24 @@ private void adaptForwardedHost(String hostToUse) {
809840
}
810841
}
811842

843+
private void adaptForwardedForHost(String hostToUse) {
844+
String hostName = hostToUse;
845+
int portSeparatorIdx = hostToUse.lastIndexOf(':');
846+
if (portSeparatorIdx > hostToUse.lastIndexOf(']')) {
847+
String hostPort = hostToUse.substring(portSeparatorIdx + 1);
848+
// check if port is not obfuscated
849+
if (hostPort.matches(FORWARDED_FOR_NUMERIC_PORT_PATTERN)) {
850+
port(Integer.parseInt(hostPort));
851+
}
852+
hostName = hostToUse.substring(0, portSeparatorIdx);
853+
}
854+
if (hostName.matches(HOST_IPV6_PATTERN)) {
855+
host(hostName.substring(hostName.indexOf('[') + 1, hostName.indexOf(']')));
856+
} else {
857+
host(hostName);
858+
}
859+
}
860+
812861
private void resetHierarchicalComponents() {
813862
this.userInfo = null;
814863
this.host = null;

0 commit comments

Comments
 (0)