Skip to content

Commit 793581e

Browse files
committed
Add ForwardedHeaderUtils
Closes gh-30886
1 parent cc7f310 commit 793581e

File tree

7 files changed

+797
-669
lines changed

7 files changed

+797
-669
lines changed

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

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -97,35 +97,45 @@ public HttpMethod getMethod() {
9797
@Override
9898
public URI getURI() {
9999
if (this.uri == null) {
100-
String urlString = null;
101-
boolean hasQuery = false;
100+
this.uri = initURI(this.servletRequest);
101+
}
102+
return this.uri;
103+
}
104+
105+
/**
106+
* Initialize a URI from the given Servet request.
107+
* @param servletRequest the request
108+
* @return the initialized URI
109+
* @since 6.1
110+
*/
111+
public static URI initURI(HttpServletRequest servletRequest) {
112+
String urlString = null;
113+
boolean hasQuery = false;
114+
try {
115+
StringBuffer url = servletRequest.getRequestURL();
116+
String query = servletRequest.getQueryString();
117+
hasQuery = StringUtils.hasText(query);
118+
if (hasQuery) {
119+
url.append('?').append(query);
120+
}
121+
urlString = url.toString();
122+
return new URI(urlString);
123+
}
124+
catch (URISyntaxException ex) {
125+
if (!hasQuery) {
126+
throw new IllegalStateException(
127+
"Could not resolve HttpServletRequest as URI: " + urlString, ex);
128+
}
129+
// Maybe a malformed query string... try plain request URL
102130
try {
103-
StringBuffer url = this.servletRequest.getRequestURL();
104-
String query = this.servletRequest.getQueryString();
105-
hasQuery = StringUtils.hasText(query);
106-
if (hasQuery) {
107-
url.append('?').append(query);
108-
}
109-
urlString = url.toString();
110-
this.uri = new URI(urlString);
131+
urlString = servletRequest.getRequestURL().toString();
132+
return new URI(urlString);
111133
}
112-
catch (URISyntaxException ex) {
113-
if (!hasQuery) {
114-
throw new IllegalStateException(
115-
"Could not resolve HttpServletRequest as URI: " + urlString, ex);
116-
}
117-
// Maybe a malformed query string... try plain request URL
118-
try {
119-
urlString = this.servletRequest.getRequestURL().toString();
120-
this.uri = new URI(urlString);
121-
}
122-
catch (URISyntaxException ex2) {
123-
throw new IllegalStateException(
124-
"Could not resolve HttpServletRequest as URI: " + urlString, ex2);
125-
}
134+
catch (URISyntaxException ex2) {
135+
throw new IllegalStateException(
136+
"Could not resolve HttpServletRequest as URI: " + urlString, ex2);
126137
}
127138
}
128-
return this.uri;
129139
}
130140

131141
@Override

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

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2021 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -18,6 +18,7 @@
1818

1919
import java.io.IOException;
2020
import java.net.InetSocketAddress;
21+
import java.net.URI;
2122
import java.util.Collections;
2223
import java.util.Enumeration;
2324
import java.util.Locale;
@@ -31,12 +32,14 @@
3132
import jakarta.servlet.http.HttpServletResponse;
3233
import jakarta.servlet.http.HttpServletResponseWrapper;
3334

35+
import org.springframework.http.HttpHeaders;
3436
import org.springframework.http.HttpStatus;
3537
import org.springframework.http.server.ServerHttpRequest;
3638
import org.springframework.http.server.ServletServerHttpRequest;
3739
import org.springframework.lang.Nullable;
3840
import org.springframework.util.LinkedCaseInsensitiveMap;
3941
import org.springframework.util.StringUtils;
42+
import org.springframework.web.util.ForwardedHeaderUtils;
4043
import org.springframework.web.util.UriComponents;
4144
import org.springframework.web.util.UriComponentsBuilder;
4245
import org.springframework.web.util.UrlPathHelper;
@@ -236,15 +239,17 @@ private static class ForwardedHeaderExtractingRequest extends ForwardedHeaderRem
236239
super(servletRequest);
237240

238241
ServerHttpRequest request = new ServletServerHttpRequest(servletRequest);
239-
UriComponents uriComponents = UriComponentsBuilder.fromHttpRequest(request).build();
242+
URI uri = request.getURI();
243+
HttpHeaders headers = request.getHeaders();
244+
UriComponents uriComponents = ForwardedHeaderUtils.adaptFromForwardedHeaders(uri, headers).build();
240245
int port = uriComponents.getPort();
241246

242247
this.scheme = uriComponents.getScheme();
243248
this.secure = "https".equals(this.scheme) || "wss".equals(this.scheme);
244249
this.host = uriComponents.getHost();
245250
this.port = (port == -1 ? (this.secure ? 443 : 80) : port);
246251

247-
this.remoteAddress = UriComponentsBuilder.parseForwardedFor(request, request.getRemoteAddress());
252+
this.remoteAddress = ForwardedHeaderUtils.parseForwardedFor(uri, headers, request.getRemoteAddress());
248253

249254
String baseUrl = this.scheme + "://" + this.host + (port == -1 ? "" : ":" + port);
250255
Supplier<HttpServletRequest> delegateRequest = () -> (HttpServletRequest) getRequest();
@@ -453,8 +458,11 @@ public void sendRedirect(String location) throws IOException {
453458
StringUtils.applyRelativePath(this.request.getRequestURI(), path));
454459
}
455460

456-
String result = UriComponentsBuilder
457-
.fromHttpRequest(new ServletServerHttpRequest(this.request))
461+
ServletServerHttpRequest httpRequest = new ServletServerHttpRequest(this.request);
462+
URI uri = httpRequest.getURI();
463+
HttpHeaders headers = httpRequest.getHeaders();
464+
465+
String result = ForwardedHeaderUtils.adaptFromForwardedHeaders(uri, headers)
458466
.replacePath(path)
459467
.replaceQuery(uriComponents.getQuery())
460468
.fragment(uriComponents.getFragment())

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2020 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -29,7 +29,7 @@
2929
import org.springframework.lang.Nullable;
3030
import org.springframework.util.LinkedCaseInsensitiveMap;
3131
import org.springframework.util.StringUtils;
32-
import org.springframework.web.util.UriComponentsBuilder;
32+
import org.springframework.web.util.ForwardedHeaderUtils;
3333

3434
/**
3535
* Extract values from "Forwarded" and "X-Forwarded-*" headers to override
@@ -100,15 +100,17 @@ public ServerHttpRequest apply(ServerHttpRequest request) {
100100
if (hasForwardedHeaders(request)) {
101101
ServerHttpRequest.Builder builder = request.mutate();
102102
if (!this.removeOnly) {
103-
URI uri = UriComponentsBuilder.fromHttpRequest(request).build(true).toUri();
103+
URI originalUri = request.getURI();
104+
HttpHeaders headers = request.getHeaders();
105+
URI uri = ForwardedHeaderUtils.adaptFromForwardedHeaders(originalUri, headers).build(true).toUri();
104106
builder.uri(uri);
105107
String prefix = getForwardedPrefix(request);
106108
if (prefix != null) {
107109
builder.path(prefix + uri.getRawPath());
108110
builder.contextPath(prefix);
109111
}
110112
InetSocketAddress remoteAddress = request.getRemoteAddress();
111-
remoteAddress = UriComponentsBuilder.parseForwardedFor(request, remoteAddress);
113+
remoteAddress = ForwardedHeaderUtils.parseForwardedFor(originalUri, headers, remoteAddress);
112114
if (remoteAddress != null) {
113115
builder.remoteAddress(remoteAddress);
114116
}
Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/*
2+
* Copyright 2002-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.web.util;
18+
19+
import java.net.InetSocketAddress;
20+
import java.net.URI;
21+
import java.util.regex.Matcher;
22+
import java.util.regex.Pattern;
23+
24+
import org.springframework.http.HttpHeaders;
25+
import org.springframework.lang.Nullable;
26+
import org.springframework.util.StringUtils;
27+
28+
29+
/**
30+
* Utility class to assist with processing "Forwarded" and "X-Forwarded-*" headers.
31+
*
32+
* <p><strong>Note:</strong>There are security considerations surrounding the use
33+
* of forwarded headers. Those should not be used unless the application is
34+
* behind a trusted proxy that inserts them and also explicitly removes any such
35+
* headers coming from an external source.
36+
*
37+
* <p>In most cases, should not use this class directly but rely on
38+
* {@link org.springframework.web.filter.ForwardedHeaderFilter} for Spring MVC, or
39+
* {@link org.springframework.web.server.adapter.ForwardedHeaderTransformer} in
40+
* order to extract the information from them as early as possible, and discard
41+
* such headers. Underlying servers such as Tomcat, Jetty, Reactor Netty, also
42+
* provides options to handle forwarded headers even earlier.
43+
*
44+
* @author Rossen Stoyanchev
45+
* @since 6.1
46+
*/
47+
public abstract class ForwardedHeaderUtils {
48+
49+
private static final String FORWARDED_VALUE = "\"?([^;,\"]+)\"?";
50+
51+
private static final Pattern FORWARDED_HOST_PATTERN = Pattern.compile("(?i:host)=" + FORWARDED_VALUE);
52+
53+
private static final Pattern FORWARDED_PROTO_PATTERN = Pattern.compile("(?i:proto)=" + FORWARDED_VALUE);
54+
55+
private static final Pattern FORWARDED_FOR_PATTERN = Pattern.compile("(?i:for)=" + FORWARDED_VALUE);
56+
57+
58+
/**
59+
* Adapt the scheme+host+port of the given {@link URI} from the "Forwarded" header,
60+
* see <a href="https://tools.ietf.org/html/rfc7239">RFC 7239</a>, or
61+
* "X-Forwarded-Host", "X-Forwarded-Port", and "X-Forwarded-Proto" if "Forwarded"
62+
* is not present.
63+
* @param headers the HTTP headers to consider
64+
* @return a {@link UriComponentsBuilder} that reflects the request URI and
65+
* additional updates from forwarded headers
66+
*/
67+
public static UriComponentsBuilder adaptFromForwardedHeaders(URI uri, HttpHeaders headers) {
68+
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(uri);
69+
try {
70+
String forwardedHeader = headers.getFirst("Forwarded");
71+
if (StringUtils.hasText(forwardedHeader)) {
72+
Matcher matcher = FORWARDED_PROTO_PATTERN.matcher(forwardedHeader);
73+
if (matcher.find()) {
74+
uriComponentsBuilder.scheme(matcher.group(1).trim());
75+
uriComponentsBuilder.port(null);
76+
}
77+
else if (isForwardedSslOn(headers)) {
78+
uriComponentsBuilder.scheme("https");
79+
uriComponentsBuilder.port(null);
80+
}
81+
matcher = FORWARDED_HOST_PATTERN.matcher(forwardedHeader);
82+
if (matcher.find()) {
83+
adaptForwardedHost(uriComponentsBuilder, matcher.group(1).trim());
84+
}
85+
}
86+
else {
87+
String protocolHeader = headers.getFirst("X-Forwarded-Proto");
88+
if (StringUtils.hasText(protocolHeader)) {
89+
uriComponentsBuilder.scheme(StringUtils.tokenizeToStringArray(protocolHeader, ",")[0]);
90+
uriComponentsBuilder.port(null);
91+
}
92+
else if (isForwardedSslOn(headers)) {
93+
uriComponentsBuilder.scheme("https");
94+
uriComponentsBuilder.port(null);
95+
}
96+
String hostHeader = headers.getFirst("X-Forwarded-Host");
97+
if (StringUtils.hasText(hostHeader)) {
98+
adaptForwardedHost(uriComponentsBuilder, StringUtils.tokenizeToStringArray(hostHeader, ",")[0]);
99+
}
100+
String portHeader = headers.getFirst("X-Forwarded-Port");
101+
if (StringUtils.hasText(portHeader)) {
102+
uriComponentsBuilder.port(Integer.parseInt(StringUtils.tokenizeToStringArray(portHeader, ",")[0]));
103+
}
104+
}
105+
}
106+
catch (NumberFormatException ex) {
107+
throw new IllegalArgumentException("Failed to parse a port from \"forwarded\"-type headers. " +
108+
"If not behind a trusted proxy, consider using ForwardedHeaderFilter " +
109+
"with the removeOnly=true. Request headers: " + headers);
110+
}
111+
112+
uriComponentsBuilder.resetPortIfDefaultForScheme();
113+
114+
return uriComponentsBuilder;
115+
}
116+
117+
private static boolean isForwardedSslOn(HttpHeaders headers) {
118+
String forwardedSsl = headers.getFirst("X-Forwarded-Ssl");
119+
return StringUtils.hasText(forwardedSsl) && forwardedSsl.equalsIgnoreCase("on");
120+
}
121+
122+
private static void adaptForwardedHost(UriComponentsBuilder uriComponentsBuilder, String rawValue) {
123+
int portSeparatorIdx = rawValue.lastIndexOf(':');
124+
int squareBracketIdx = rawValue.lastIndexOf(']');
125+
if (portSeparatorIdx > squareBracketIdx) {
126+
if (squareBracketIdx == -1 && rawValue.indexOf(':') != portSeparatorIdx) {
127+
throw new IllegalArgumentException("Invalid IPv4 address: " + rawValue);
128+
}
129+
uriComponentsBuilder.host(rawValue.substring(0, portSeparatorIdx));
130+
uriComponentsBuilder.port(Integer.parseInt(rawValue, portSeparatorIdx + 1, rawValue.length(), 10));
131+
}
132+
else {
133+
uriComponentsBuilder.host(rawValue);
134+
uriComponentsBuilder.port(null);
135+
}
136+
}
137+
138+
/**
139+
* Parse the first "Forwarded: for=..." or "X-Forwarded-For" header value to
140+
* an {@code InetSocketAddress} representing the address of the client.
141+
* @param uri the request URI
142+
* @param headers the request headers that may contain forwarded headers
143+
* @param remoteAddress the current remoteAddress
144+
* @return an {@code InetSocketAddress} with the extracted host and port, or
145+
* {@code null} if the headers are not present.
146+
* @see <a href="https://tools.ietf.org/html/rfc7239#section-5.2">RFC 7239, Section 5.2</a>
147+
*/
148+
@Nullable
149+
public static InetSocketAddress parseForwardedFor(
150+
URI uri, HttpHeaders headers, @Nullable InetSocketAddress remoteAddress) {
151+
152+
int port = (remoteAddress != null ?
153+
remoteAddress.getPort() : "https".equals(uri.getScheme()) ? 443 : 80);
154+
155+
String forwardedHeader = headers.getFirst("Forwarded");
156+
if (StringUtils.hasText(forwardedHeader)) {
157+
String forwardedToUse = StringUtils.tokenizeToStringArray(forwardedHeader, ",")[0];
158+
Matcher matcher = FORWARDED_FOR_PATTERN.matcher(forwardedToUse);
159+
if (matcher.find()) {
160+
String value = matcher.group(1).trim();
161+
String host = value;
162+
int portSeparatorIdx = value.lastIndexOf(':');
163+
int squareBracketIdx = value.lastIndexOf(']');
164+
if (portSeparatorIdx > squareBracketIdx) {
165+
if (squareBracketIdx == -1 && value.indexOf(':') != portSeparatorIdx) {
166+
throw new IllegalArgumentException("Invalid IPv4 address: " + value);
167+
}
168+
host = value.substring(0, portSeparatorIdx);
169+
try {
170+
port = Integer.parseInt(value, portSeparatorIdx + 1, value.length(), 10);
171+
}
172+
catch (NumberFormatException ex) {
173+
throw new IllegalArgumentException(
174+
"Failed to parse a port from \"forwarded\"-type header value: " + value);
175+
}
176+
}
177+
return InetSocketAddress.createUnresolved(host, port);
178+
}
179+
}
180+
181+
String forHeader = headers.getFirst("X-Forwarded-For");
182+
if (StringUtils.hasText(forHeader)) {
183+
String host = StringUtils.tokenizeToStringArray(forHeader, ",")[0];
184+
return InetSocketAddress.createUnresolved(host, port);
185+
}
186+
187+
return null;
188+
}
189+
190+
}

0 commit comments

Comments
 (0)