Skip to content

Commit cf8032c

Browse files
committedMar 29, 2025
Add support for X-Forwarded-By and Forwarded By
See spring-projectsgh-34654, spring-projectsgh-23260 Signed-off-by: Mengqi Xu <[email protected]>
1 parent ac7c7ff commit cf8032c

File tree

7 files changed

+219
-3
lines changed

7 files changed

+219
-3
lines changed
 

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

+14-3
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,8 @@ class DefaultServerHttpRequestBuilder implements ServerHttpRequest.Builder {
5757

5858
private @Nullable InetSocketAddress remoteAddress;
5959

60+
private @Nullable InetSocketAddress localAddress;
61+
6062
private final Flux<DataBuffer> body;
6163

6264
private final ServerHttpRequest originalRequest;
@@ -131,10 +133,16 @@ public ServerHttpRequest.Builder remoteAddress(InetSocketAddress remoteAddress)
131133
return this;
132134
}
133135

136+
@Override
137+
public ServerHttpRequest.Builder localAddress(InetSocketAddress localAddress) {
138+
this.localAddress = localAddress;
139+
return this;
140+
}
141+
134142
@Override
135143
public ServerHttpRequest build() {
136144
return new MutatedServerHttpRequest(getUriToUse(), this.contextPath,
137-
this.httpMethod, this.sslInfo, this.remoteAddress, this.headers, this.body, this.originalRequest);
145+
this.httpMethod, this.sslInfo, this.remoteAddress, this.localAddress, this.headers, this.body, this.originalRequest);
138146
}
139147

140148
private URI getUriToUse() {
@@ -182,16 +190,19 @@ private static class MutatedServerHttpRequest extends AbstractServerHttpRequest
182190

183191
private final @Nullable InetSocketAddress remoteAddress;
184192

193+
private final @Nullable InetSocketAddress localAddress;
194+
185195
private final Flux<DataBuffer> body;
186196

187197
private final ServerHttpRequest originalRequest;
188198

189199
public MutatedServerHttpRequest(URI uri, @Nullable String contextPath,
190-
HttpMethod method, @Nullable SslInfo sslInfo, @Nullable InetSocketAddress remoteAddress,
200+
HttpMethod method, @Nullable SslInfo sslInfo, @Nullable InetSocketAddress remoteAddress, @Nullable InetSocketAddress localAddress,
191201
HttpHeaders headers, Flux<DataBuffer> body, ServerHttpRequest originalRequest) {
192202

193203
super(method, uri, contextPath, headers);
194204
this.remoteAddress = (remoteAddress != null ? remoteAddress : originalRequest.getRemoteAddress());
205+
this.localAddress = (localAddress != null ? localAddress : originalRequest.getLocalAddress());
195206
this.sslInfo = (sslInfo != null ? sslInfo : originalRequest.getSslInfo());
196207
this.body = body;
197208
this.originalRequest = originalRequest;
@@ -204,7 +215,7 @@ protected MultiValueMap<String, HttpCookie> initCookies() {
204215

205216
@Override
206217
public @Nullable InetSocketAddress getLocalAddress() {
207-
return this.originalRequest.getLocalAddress();
218+
return this.localAddress;
208219
}
209220

210221
@Override

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

+6
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,12 @@ interface Builder {
184184
*/
185185
Builder remoteAddress(InetSocketAddress remoteAddress);
186186

187+
/**
188+
* Set the address of the local client.
189+
* @since 7.x
190+
*/
191+
Builder localAddress(InetSocketAddress localAddress);
192+
187193
/**
188194
* Build a {@link ServerHttpRequest} decorator with the mutated properties.
189195
*/

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

+15
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@
7373
* @author Eddú Meléndez
7474
* @author Rob Winch
7575
* @author Brian Clozel
76+
* @author Mengqi Xu
7677
* @since 4.3
7778
* @see <a href="https://tools.ietf.org/html/rfc7239">https://tools.ietf.org/html/rfc7239</a>
7879
* @see <a href="https://docs.spring.io/spring-framework/reference/web/webmvc/filters.html#filters-forwarded-headers">Forwarded Headers</a>
@@ -92,6 +93,7 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter {
9293
FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix");
9394
FORWARDED_HEADER_NAMES.add("X-Forwarded-Ssl");
9495
FORWARDED_HEADER_NAMES.add("X-Forwarded-For");
96+
FORWARDED_HEADER_NAMES.add("X-Forwarded-By");
9597
}
9698

9799

@@ -255,6 +257,8 @@ private static class ForwardedHeaderExtractingRequest extends ForwardedHeaderRem
255257

256258
private final @Nullable InetSocketAddress remoteAddress;
257259

260+
private final @Nullable InetSocketAddress localAddress;
261+
258262
private final ForwardedPrefixExtractor forwardedPrefixExtractor;
259263

260264
ForwardedHeaderExtractingRequest(HttpServletRequest servletRequest) {
@@ -272,6 +276,7 @@ private static class ForwardedHeaderExtractingRequest extends ForwardedHeaderRem
272276
this.port = (port == -1 ? (this.secure ? 443 : 80) : port);
273277

274278
this.remoteAddress = ForwardedHeaderUtils.parseForwardedFor(uri, headers, request.getRemoteAddress());
279+
this.localAddress = ForwardedHeaderUtils.parseForwardedBy(uri, headers, request.getLocalAddress());
275280

276281
// Use Supplier as Tomcat updates delegate request on FORWARD
277282
Supplier<HttpServletRequest> requestSupplier = () -> (HttpServletRequest) getRequest();
@@ -330,6 +335,16 @@ public int getRemotePort() {
330335
return (this.remoteAddress != null ? this.remoteAddress.getPort() : super.getRemotePort());
331336
}
332337

338+
@Override
339+
public @Nullable String getLocalAddr() {
340+
return (this.localAddress != null ? this.localAddress.getHostString() : super.getLocalAddr());
341+
}
342+
343+
@Override
344+
public int getLocalPort() {
345+
return (this.localAddress != null ? this.localAddress.getPort() : super.getLocalPort());
346+
}
347+
333348
@SuppressWarnings("DataFlowIssue")
334349
@Override
335350
public @Nullable Object getAttribute(String name) {

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

+7
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
*
5656
* @author Rossen Stoyanchev
5757
* @author Sebastien Deleuze
58+
* @author Mengqi Xu
5859
* @since 5.1
5960
* @see <a href="https://tools.ietf.org/html/rfc7239">https://tools.ietf.org/html/rfc7239</a>
6061
* @see <a href="https://docs.spring.io/spring-framework/reference/web/webflux/reactive-spring.html#webflux-forwarded-headers">Forwarded Headers</a>
@@ -72,6 +73,7 @@ public class ForwardedHeaderTransformer implements Function<ServerHttpRequest, S
7273
FORWARDED_HEADER_NAMES.add("X-Forwarded-Prefix");
7374
FORWARDED_HEADER_NAMES.add("X-Forwarded-Ssl");
7475
FORWARDED_HEADER_NAMES.add("X-Forwarded-For");
76+
FORWARDED_HEADER_NAMES.add("X-Forwarded-By");
7577
}
7678

7779

@@ -119,6 +121,11 @@ public ServerHttpRequest apply(ServerHttpRequest request) {
119121
if (remoteAddress != null) {
120122
builder.remoteAddress(remoteAddress);
121123
}
124+
InetSocketAddress localAddress = request.getLocalAddress();
125+
localAddress = ForwardedHeaderUtils.parseForwardedBy(originalUri, headers, localAddress);
126+
if (localAddress != null) {
127+
builder.localAddress(localAddress);
128+
}
122129
}
123130
removeForwardedHeaders(builder);
124131
request = builder.build();

‎spring-web/src/main/java/org/springframework/web/util/ForwardedHeaderUtils.java

+54
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public abstract class ForwardedHeaderUtils {
5454

5555
private static final Pattern FORWARDED_FOR_PATTERN = Pattern.compile("(?i:for)=" + FORWARDED_VALUE);
5656

57+
private static final Pattern FORWARDED_BY_PATTERN = Pattern.compile("(?i:by)=" + FORWARDED_VALUE);
5758

5859
/**
5960
* Adapt the scheme+host+port of the given {@link URI} from the "Forwarded" header
@@ -189,4 +190,57 @@ private static void adaptForwardedHost(UriComponentsBuilder uriComponentsBuilder
189190
return null;
190191
}
191192

193+
/**
194+
* Parse the first "Forwarded: by=..." or "X-Forwarded-By" header value to
195+
* an {@code InetSocketAddress} representing the address of the server.
196+
* @param uri the request {@code URI}
197+
* @param headers the request headers that may contain forwarded headers
198+
* @param localAddress the current local address
199+
* @return an {@code InetSocketAddress} with the extracted host and port, or
200+
* {@code null} if the headers are not present
201+
* @see <a href="https://tools.ietf.org/html/rfc7239#section-5.1">RFC 7239, Section 5.1</a>
202+
*/
203+
public static @Nullable InetSocketAddress parseForwardedBy(
204+
URI uri, HttpHeaders headers, @Nullable InetSocketAddress localAddress) {
205+
206+
int port = (localAddress != null ?
207+
localAddress.getPort() : "https".equals(uri.getScheme()) ? 443 : 80);
208+
209+
String forwardedHeader = headers.getFirst("Forwarded");
210+
if (StringUtils.hasText(forwardedHeader)) {
211+
String forwardedToUse = StringUtils.tokenizeToStringArray(forwardedHeader, ",")[0];
212+
Matcher matcher = FORWARDED_BY_PATTERN.matcher(forwardedToUse);
213+
if (matcher.find()) {
214+
String value = matcher.group(1).trim();
215+
String host = value;
216+
int portSeparatorIdx = value.lastIndexOf(':');
217+
int squareBracketIdx = value.lastIndexOf(']');
218+
if (portSeparatorIdx > squareBracketIdx) {
219+
if (squareBracketIdx == -1 && value.indexOf(':') != portSeparatorIdx) {
220+
throw new IllegalArgumentException("Invalid IPv4 address: " + value);
221+
}
222+
host = value.substring(0, portSeparatorIdx);
223+
try {
224+
port = Integer.parseInt(value, portSeparatorIdx + 1, value.length(), 10);
225+
}
226+
catch (NumberFormatException ex) {
227+
throw new IllegalArgumentException(
228+
"Failed to parse a port from \"forwarded\"-type header value: " + value);
229+
}
230+
}
231+
return InetSocketAddress.createUnresolved(host, port);
232+
}
233+
}
234+
235+
String byHeader = headers.getFirst("X-Forwarded-By");
236+
if (StringUtils.hasText(byHeader)) {
237+
String host = StringUtils.tokenizeToStringArray(byHeader, ",")[0];
238+
boolean ipv6 = (host.indexOf(':') != -1);
239+
host = (ipv6 && !host.startsWith("[") && !host.endsWith("]") ? "[" + host + "]" : host);
240+
return InetSocketAddress.createUnresolved(host, port);
241+
}
242+
243+
return null;
244+
}
245+
192246
}

‎spring-web/src/test/java/org/springframework/web/filter/ForwardedHeaderFilterTests.java

+87
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
* @author Rob Winch
5050
* @author Brian Clozel
5151
* @author Sebastien Deleuze
52+
* @author Mengqi Xu
5253
*/
5354
class ForwardedHeaderFilterTests {
5455

@@ -66,6 +67,8 @@ class ForwardedHeaderFilterTests {
6667

6768
private static final String X_FORWARDED_FOR = "x-forwarded-for";
6869

70+
private static final String X_FORWARDED_BY = "x-forwarded-by";
71+
6972

7073
private final ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
7174

@@ -93,6 +96,7 @@ void shouldFilter() {
9396
testShouldFilter(X_FORWARDED_SSL);
9497
testShouldFilter(X_FORWARDED_PREFIX);
9598
testShouldFilter(X_FORWARDED_FOR);
99+
testShouldFilter(X_FORWARDED_BY);
96100
}
97101

98102
private void testShouldFilter(String headerName) {
@@ -115,6 +119,7 @@ void forwardedRequest(String protocol) throws Exception {
115119
this.request.addHeader(X_FORWARDED_PORT, "443");
116120
this.request.addHeader("foo", "bar");
117121
this.request.addHeader(X_FORWARDED_FOR, "[203.0.113.195]");
122+
this.request.addHeader(X_FORWARDED_BY, "[203.0.113.196]");
118123

119124
this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain);
120125
HttpServletRequest actual = (HttpServletRequest) this.filterChain.getRequest();
@@ -126,11 +131,13 @@ void forwardedRequest(String protocol) throws Exception {
126131
assertThat(actual.getServerPort()).isEqualTo(443);
127132
assertThat(actual.isSecure()).isTrue();
128133
assertThat(actual.getRemoteAddr()).isEqualTo(actual.getRemoteHost()).isEqualTo("[203.0.113.195]");
134+
assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("[203.0.113.196]");
129135

130136
assertThat(actual.getHeader(X_FORWARDED_PROTO)).isNull();
131137
assertThat(actual.getHeader(X_FORWARDED_HOST)).isNull();
132138
assertThat(actual.getHeader(X_FORWARDED_PORT)).isNull();
133139
assertThat(actual.getHeader(X_FORWARDED_FOR)).isNull();
140+
assertThat(actual.getHeader(X_FORWARDED_BY)).isNull();
134141
assertThat(actual.getHeader("foo")).isEqualTo("bar");
135142
}
136143

@@ -143,6 +150,7 @@ void forwardedRequestInRemoveOnlyMode() throws Exception {
143150
this.request.addHeader(X_FORWARDED_SSL, "on");
144151
this.request.addHeader("foo", "bar");
145152
this.request.addHeader(X_FORWARDED_FOR, "203.0.113.195");
153+
this.request.addHeader(X_FORWARDED_BY, "203.0.113.196");
146154

147155
this.filter.setRemoveOnly(true);
148156
this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain);
@@ -156,12 +164,14 @@ void forwardedRequestInRemoveOnlyMode() throws Exception {
156164
assertThat(actual.isSecure()).isFalse();
157165
assertThat(actual.getRemoteAddr()).isEqualTo(MockHttpServletRequest.DEFAULT_REMOTE_ADDR);
158166
assertThat(actual.getRemoteHost()).isEqualTo(MockHttpServletRequest.DEFAULT_REMOTE_HOST);
167+
assertThat(actual.getLocalAddr()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_ADDR);
159168

160169
assertThat(actual.getHeader(X_FORWARDED_PROTO)).isNull();
161170
assertThat(actual.getHeader(X_FORWARDED_HOST)).isNull();
162171
assertThat(actual.getHeader(X_FORWARDED_PORT)).isNull();
163172
assertThat(actual.getHeader(X_FORWARDED_SSL)).isNull();
164173
assertThat(actual.getHeader(X_FORWARDED_FOR)).isNull();
174+
assertThat(actual.getHeader(X_FORWARDED_BY)).isNull();
165175
assertThat(actual.getHeader("foo")).isEqualTo("bar");
166176
}
167177

@@ -541,6 +551,83 @@ void forwardedForMultipleIdentifiers() throws Exception {
541551

542552
}
543553

554+
@Nested
555+
class ForwardedBy {
556+
557+
@Test
558+
void xForwardedForEmpty() throws Exception {
559+
request.addHeader(X_FORWARDED_BY, "");
560+
HttpServletRequest actual = filterAndGetWrappedRequest();
561+
562+
assertThat(actual.getLocalAddr()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_ADDR);
563+
assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT);
564+
}
565+
566+
@Test
567+
void xForwardedForSingleIdentifier() throws Exception {
568+
request.addHeader(X_FORWARDED_BY, "203.0.113.195");
569+
HttpServletRequest actual = filterAndGetWrappedRequest();
570+
571+
assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("203.0.113.195");
572+
assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT);
573+
}
574+
575+
@Test
576+
void xForwardedForMultipleIdentifiers() throws Exception {
577+
request.addHeader(X_FORWARDED_BY, "203.0.113.195, 70.41.3.18, 150.172.238.178");
578+
HttpServletRequest actual = filterAndGetWrappedRequest();
579+
580+
assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("203.0.113.195");
581+
assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT);
582+
}
583+
584+
@Test
585+
void forwardedForIpV4Identifier() throws Exception {
586+
request.addHeader(FORWARDED, "By=203.0.113.195");
587+
HttpServletRequest actual = filterAndGetWrappedRequest();
588+
589+
assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("203.0.113.195");
590+
assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT);
591+
}
592+
593+
@Test
594+
void forwardedForIpV6Identifier() throws Exception {
595+
request.addHeader(FORWARDED, "By=\"[2001:db8:cafe::17]\"");
596+
HttpServletRequest actual = filterAndGetWrappedRequest();
597+
598+
assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("[2001:db8:cafe::17]");
599+
assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT);
600+
}
601+
602+
@Test
603+
void forwardedForIpV4IdentifierWithPort() throws Exception {
604+
request.addHeader(FORWARDED, "By=\"203.0.113.195:47011\"");
605+
HttpServletRequest actual = filterAndGetWrappedRequest();
606+
607+
assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("203.0.113.195");
608+
assertThat(actual.getLocalPort()).isEqualTo(47011);
609+
}
610+
611+
@Test
612+
void forwardedForIpV6IdentifierWithPort() throws Exception {
613+
request.addHeader(FORWARDED, "By=\"[2001:db8:cafe::17]:47011\"");
614+
HttpServletRequest actual = filterAndGetWrappedRequest();
615+
616+
assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("[2001:db8:cafe::17]");
617+
assertThat(actual.getLocalPort()).isEqualTo(47011);
618+
}
619+
620+
@Test
621+
void forwardedForMultipleIdentifiers() throws Exception {
622+
request.addHeader(FORWARDED, "by=203.0.113.195;proto=http, by=\"[2001:db8:cafe::17]\", by=unknown");
623+
HttpServletRequest actual = filterAndGetWrappedRequest();
624+
625+
assertThat(actual.getLocalAddr()).isEqualTo(actual.getLocalAddr()).isEqualTo("203.0.113.195");
626+
assertThat(actual.getLocalPort()).isEqualTo(MockHttpServletRequest.DEFAULT_SERVER_PORT);
627+
}
628+
629+
}
630+
544631
@Nested
545632
class SendRedirect {
546633

‎spring-web/src/test/java/org/springframework/web/server/adapter/ForwardedHeaderTransformerTests.java

+36
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
*
3434
* @author Rossen Stoyanchev
3535
* @author Sebastien Deleuze
36+
* @author Mengqi Xu
3637
*/
3738
class ForwardedHeaderTransformerTests {
3839

@@ -52,6 +53,7 @@ void removeOnly() {
5253
headers.add("X-Forwarded-Prefix", "prefix");
5354
headers.add("X-Forwarded-Ssl", "on");
5455
headers.add("X-Forwarded-For", "203.0.113.195");
56+
headers.add("X-Forwarded-By", "203.0.113.196");
5557
ServerHttpRequest request = this.requestMutator.apply(getRequest(headers));
5658

5759
assertForwardedHeadersRemoved(request);
@@ -233,6 +235,40 @@ void xForwardedFor() {
233235
assertThat(request.getRemoteAddress().getHostName()).isEqualTo("203.0.113.195");
234236
}
235237

238+
@Test
239+
void forwardedBy() {
240+
HttpHeaders headers = new HttpHeaders();
241+
headers.add("Forwarded", "by=\"203.0.113.195:4711\";host=84.198.58.199;proto=https");
242+
243+
InetSocketAddress localAddress = new InetSocketAddress("example.client", 47011);
244+
245+
ServerHttpRequest request = MockServerHttpRequest
246+
.method(HttpMethod.GET, URI.create("https://example.com/a%20b?q=a%2Bb"))
247+
.localAddress(localAddress)
248+
.headers(headers)
249+
.build();
250+
251+
request = this.requestMutator.apply(request);
252+
assertThat(request.getLocalAddress()).isNotNull();
253+
assertThat(request.getLocalAddress().getHostName()).isEqualTo("203.0.113.195");
254+
assertThat(request.getLocalAddress().getPort()).isEqualTo(4711);
255+
}
256+
257+
@Test
258+
void xForwardedBy() {
259+
HttpHeaders headers = new HttpHeaders();
260+
headers.add("x-forwarded-by", "203.0.113.195, 70.41.3.18, 150.172.238.178");
261+
262+
ServerHttpRequest request = MockServerHttpRequest
263+
.method(HttpMethod.GET, URI.create("https://example.com/a%20b?q=a%2Bb"))
264+
.headers(headers)
265+
.build();
266+
267+
request = this.requestMutator.apply(request);
268+
assertThat(request.getLocalAddress()).isNotNull();
269+
assertThat(request.getLocalAddress().getHostName()).isEqualTo("203.0.113.195");
270+
}
271+
236272

237273
private MockServerHttpRequest getRequest(HttpHeaders headers) {
238274
return MockServerHttpRequest.get(BASE_URL).headers(headers).build();

0 commit comments

Comments
 (0)
Please sign in to comment.