Skip to content

Commit 2b19ce9

Browse files
author
Kirill Serebrennikov
committed
Add support for X-Forwarded-For and Forwarded for
Closes gh-23260
1 parent facdbdb commit 2b19ce9

File tree

7 files changed

+323
-8
lines changed

7 files changed

+323
-8
lines changed

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

+14-3
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder {
6161
@Nullable
6262
private SslInfo sslInfo;
6363

64+
@Nullable
65+
private InetSocketAddress remoteAddress;
66+
6467
private Flux<DataBuffer> body;
6568

6669
private final ServerHttpRequest originalRequest;
@@ -130,10 +133,17 @@ public ServerHttpRequest.Builder sslInfo(SslInfo sslInfo) {
130133
return this;
131134
}
132135

136+
@Override
137+
public ServerHttpRequest.Builder remoteAddress(InetSocketAddress remoteAddress) {
138+
this.remoteAddress = remoteAddress;
139+
return this;
140+
}
141+
133142
@Override
134143
public ServerHttpRequest build() {
135144
return new MutatedServerHttpRequest(getUriToUse(), this.contextPath, this.httpHeaders,
136-
this.httpMethodValue, this.cookies, this.sslInfo, this.body, this.originalRequest);
145+
this.httpMethodValue, this.cookies, this.remoteAddress, this.sslInfo, this.body,
146+
this.originalRequest);
137147
}
138148

139149
private URI getUriToUse() {
@@ -194,12 +204,13 @@ private static class MutatedServerHttpRequest extends AbstractServerHttpRequest
194204

195205
public MutatedServerHttpRequest(URI uri, @Nullable String contextPath,
196206
HttpHeaders headers, String methodValue, MultiValueMap<String, HttpCookie> cookies,
197-
@Nullable SslInfo sslInfo, Flux<DataBuffer> body, ServerHttpRequest originalRequest) {
207+
@Nullable InetSocketAddress remoteAddress, @Nullable SslInfo sslInfo, Flux<DataBuffer> body,
208+
ServerHttpRequest originalRequest) {
198209

199210
super(uri, contextPath, headers);
200211
this.methodValue = methodValue;
201212
this.cookies = cookies;
202-
this.remoteAddress = originalRequest.getRemoteAddress();
213+
this.remoteAddress = remoteAddress;
203214
this.sslInfo = sslInfo != null ? sslInfo : originalRequest.getSslInfo();
204215
this.body = body;
205216
this.originalRequest = originalRequest;

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

+5
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,11 @@ interface Builder {
166166
*/
167167
Builder sslInfo(SslInfo sslInfo);
168168

169+
/**
170+
* Set the address of the remote client.
171+
*/
172+
Builder remoteAddress(InetSocketAddress remoteAddress);
173+
169174
/**
170175
* Build a {@link ServerHttpRequest} decorator with the mutated properties.
171176
*/

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

+52-1
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

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

+30-1
Original file line numberDiff line numberDiff line change
@@ -16,17 +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;
32+
import org.springframework.util.StringUtils;
33+
import org.springframework.web.util.UriComponents;
3034
import org.springframework.web.util.UriComponentsBuilder;
3135

3236
/**
@@ -49,8 +53,11 @@
4953
*/
5054
public class ForwardedHeaderTransformer implements Function<ServerHttpRequest, ServerHttpRequest> {
5155

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=.+)");
5259
static final Set<String> FORWARDED_HEADER_NAMES =
53-
Collections.newSetFromMap(new LinkedCaseInsensitiveMap<>(8, Locale.ENGLISH));
60+
Collections.newSetFromMap(new LinkedCaseInsensitiveMap<>(10, Locale.ENGLISH));
5461

5562
static {
5663
FORWARDED_HEADER_NAMES.add("Forwarded");
@@ -59,6 +66,7 @@ public class ForwardedHeaderTransformer implements Function<ServerHttpRequest, S
5966
FORWARDED_HEADER_NAMES.add("X-Forwarded-Proto");
6067
FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix");
6168
FORWARDED_HEADER_NAMES.add("X-Forwarded-Ssl");
69+
FORWARDED_HEADER_NAMES.add("X-Forwarded-For");
6270
}
6371

6472

@@ -99,6 +107,27 @@ public ServerHttpRequest apply(ServerHttpRequest request) {
99107
builder.path(prefix + uri.getRawPath());
100108
builder.contextPath(prefix);
101109
}
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+
}
102131
}
103132
removeForwardedHeaders(builder);
104133
request = builder.build();

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

+51-2
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

@@ -832,6 +836,33 @@ public UriComponentsBuilder uriVariables(Map<String, Object> uriVariables) {
832836
return this;
833837
}
834838

839+
/**
840+
* Adapt this builders's host+port from the "for" parameter of the "Forwarded"
841+
* header or from "X-Forwarded-For" if "Forwarded" is not found. If neither
842+
* "Forwarded" nor "X-Forwarded-For" is found no changes are made to the
843+
* builder.
844+
* @param headers the HTTP headers to consider
845+
* @return this UriComponentsBuilder
846+
*/
847+
public UriComponentsBuilder adaptFromForwardedForHeader(HttpHeaders headers) {
848+
String forwardedHeader = headers.getFirst("Forwarded");
849+
if (StringUtils.hasText(forwardedHeader)) {
850+
String forwardedToUse = StringUtils.tokenizeToStringArray(forwardedHeader, ",")[0];
851+
Matcher matcher = FORWARDED_FOR_PATTERN.matcher(forwardedToUse);
852+
if (matcher.find()) {
853+
adaptForwardedForHost(matcher.group(1).trim());
854+
}
855+
}
856+
else {
857+
String forHeader = headers.getFirst("X-Forwarded-For");
858+
if (StringUtils.hasText(forHeader)) {
859+
String forwardedForToUse = StringUtils.tokenizeToStringArray(forHeader, ",")[0];
860+
host(forwardedForToUse);
861+
}
862+
}
863+
return this;
864+
}
865+
835866
/**
836867
* Adapt this builder's scheme+host+port from the given headers, specifically
837868
* "Forwarded" (<a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>,
@@ -918,6 +949,24 @@ private void adaptForwardedHost(String hostToUse) {
918949
}
919950
}
920951

952+
private void adaptForwardedForHost(String hostToUse) {
953+
String hostName = hostToUse;
954+
int portSeparatorIdx = hostToUse.lastIndexOf(':');
955+
if (portSeparatorIdx > hostToUse.lastIndexOf(']')) {
956+
String hostPort = hostToUse.substring(portSeparatorIdx + 1);
957+
// check if port is not obfuscated
958+
if (hostPort.matches(FORWARDED_FOR_NUMERIC_PORT_PATTERN)) {
959+
port(Integer.parseInt(hostPort));
960+
}
961+
hostName = hostToUse.substring(0, portSeparatorIdx);
962+
}
963+
if (hostName.matches(HOST_IPV6_PATTERN)) {
964+
host(hostName.substring(hostName.indexOf('[') + 1, hostName.indexOf(']')));
965+
} else {
966+
host(hostName);
967+
}
968+
}
969+
921970
private void resetHierarchicalComponents() {
922971
this.userInfo = null;
923972
this.host = null;

0 commit comments

Comments
 (0)