Skip to content

Commit 6615e9c

Browse files
committed
Support multi-value X-Forwarded-Prefix headers
Prior to this commit, the Forwarded headers for Spring MVC and Spring WebFlux did not support multiple prefix values for the `"X-Forwarded-Prefix"` HTTP header. This commit splits and processes multiple prefixes defined in the dedicated header. Closes gh-25254
1 parent bad81ce commit 6615e9c

File tree

4 files changed

+77
-21
lines changed

4 files changed

+77
-21
lines changed

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,11 +343,18 @@ private static String initForwardedPrefix(HttpServletRequest request) {
343343
}
344344
}
345345
if (result != null) {
346-
while (result.endsWith("/")) {
347-
result = result.substring(0, result.length() - 1);
346+
StringBuilder prefix = new StringBuilder(result.length());
347+
String[] rawPrefixes = StringUtils.tokenizeToStringArray(result, ",");
348+
for (String rawPrefix : rawPrefixes) {
349+
int endIndex = rawPrefix.length();
350+
while (endIndex > 0 && rawPrefix.charAt(endIndex - 1) == '/') {
351+
endIndex--;
352+
}
353+
prefix.append((endIndex != rawPrefix.length() ? rawPrefix.substring(0, endIndex) : rawPrefix));
348354
}
355+
return prefix.toString();
349356
}
350-
return result;
357+
return null;
351358
}
352359

353360
@Nullable

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

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 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.
@@ -27,6 +27,7 @@
2727
import org.springframework.http.server.reactive.ServerHttpRequest;
2828
import org.springframework.lang.Nullable;
2929
import org.springframework.util.LinkedCaseInsensitiveMap;
30+
import org.springframework.util.StringUtils;
3031
import org.springframework.web.util.UriComponentsBuilder;
3132

3233
/**
@@ -128,15 +129,20 @@ private void removeForwardedHeaders(ServerHttpRequest.Builder builder) {
128129
@Nullable
129130
private static String getForwardedPrefix(ServerHttpRequest request) {
130131
HttpHeaders headers = request.getHeaders();
131-
String prefix = headers.getFirst("X-Forwarded-Prefix");
132-
if (prefix != null) {
133-
int endIndex = prefix.length();
134-
while (endIndex > 1 && prefix.charAt(endIndex - 1) == '/') {
135-
endIndex--;
132+
String header = headers.getFirst("X-Forwarded-Prefix");
133+
if (header != null) {
134+
StringBuilder prefix = new StringBuilder(header.length());
135+
String[] rawPrefixes = StringUtils.tokenizeToStringArray(header, ",");
136+
for (String rawPrefix : rawPrefixes) {
137+
int endIndex = rawPrefix.length();
138+
while (endIndex > 1 && rawPrefix.charAt(endIndex - 1) == '/') {
139+
endIndex--;
140+
}
141+
prefix.append((endIndex != rawPrefix.length() ? rawPrefix.substring(0, endIndex) : rawPrefix));
136142
}
137-
prefix = (endIndex != prefix.length() ? prefix.substring(0, endIndex) : prefix);
143+
return prefix.toString();
138144
}
139-
return prefix;
145+
return header;
140146
}
141147

142148
}

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 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.
@@ -47,9 +47,13 @@
4747
public class ForwardedHeaderFilterTests {
4848

4949
private static final String X_FORWARDED_PROTO = "x-forwarded-proto"; // SPR-14372 (case insensitive)
50+
5051
private static final String X_FORWARDED_HOST = "x-forwarded-host";
52+
5153
private static final String X_FORWARDED_PORT = "x-forwarded-port";
54+
5255
private static final String X_FORWARDED_PREFIX = "x-forwarded-prefix";
56+
5357
private static final String X_FORWARDED_SSL = "x-forwarded-ssl";
5458

5559

@@ -350,6 +354,24 @@ public void requestUriWithForwardedPrefixTrailingSlash() throws Exception {
350354
assertThat(actual.getRequestURL().toString()).isEqualTo("http://localhost/prefix/mvc-showcase");
351355
}
352356

357+
@Test
358+
void shouldConcatenatePrefixes() throws Exception {
359+
this.request.addHeader(X_FORWARDED_PREFIX, "/first,/second");
360+
this.request.setRequestURI("/mvc-showcase");
361+
362+
HttpServletRequest actual = filterAndGetWrappedRequest();
363+
assertThat(actual.getRequestURL().toString()).isEqualTo("http://localhost/first/second/mvc-showcase");
364+
}
365+
366+
@Test
367+
void shouldConcatenatePrefixesWithTrailingSlashes() throws Exception {
368+
this.request.addHeader(X_FORWARDED_PREFIX, "/first/,/second//");
369+
this.request.setRequestURI("/mvc-showcase");
370+
371+
HttpServletRequest actual = filterAndGetWrappedRequest();
372+
assertThat(actual.getRequestURL().toString()).isEqualTo("http://localhost/first/second/mvc-showcase");
373+
}
374+
353375
@Test
354376
public void requestURLNewStringBuffer() throws Exception {
355377
this.request.addHeader(X_FORWARDED_PREFIX, "/prefix/");

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

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2019 the original author or authors.
2+
* Copyright 2002-2020 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.
@@ -40,8 +40,7 @@ public class ForwardedHeaderTransformerTests {
4040

4141

4242
@Test
43-
public void removeOnly() {
44-
43+
void removeOnly() {
4544
this.requestMutator.setRemoveOnly(true);
4645

4746
HttpHeaders headers = new HttpHeaders();
@@ -57,7 +56,7 @@ public void removeOnly() {
5756
}
5857

5958
@Test
60-
public void xForwardedHeaders() throws Exception {
59+
void xForwardedHeaders() throws Exception {
6160
HttpHeaders headers = new HttpHeaders();
6261
headers.add("X-Forwarded-Host", "84.198.58.199");
6362
headers.add("X-Forwarded-Port", "443");
@@ -70,7 +69,7 @@ public void xForwardedHeaders() throws Exception {
7069
}
7170

7271
@Test
73-
public void forwardedHeader() throws Exception {
72+
void forwardedHeader() throws Exception {
7473
HttpHeaders headers = new HttpHeaders();
7574
headers.add("Forwarded", "host=84.198.58.199;proto=https");
7675
ServerHttpRequest request = this.requestMutator.apply(getRequest(headers));
@@ -80,7 +79,7 @@ public void forwardedHeader() throws Exception {
8079
}
8180

8281
@Test
83-
public void xForwardedPrefix() throws Exception {
82+
void xForwardedPrefix() throws Exception {
8483
HttpHeaders headers = new HttpHeaders();
8584
headers.add("X-Forwarded-Prefix", "/prefix");
8685
ServerHttpRequest request = this.requestMutator.apply(getRequest(headers));
@@ -91,7 +90,7 @@ public void xForwardedPrefix() throws Exception {
9190
}
9291

9392
@Test // gh-23305
94-
public void xForwardedPrefixShouldNotLeadToDecodedPath() throws Exception {
93+
void xForwardedPrefixShouldNotLeadToDecodedPath() throws Exception {
9594
HttpHeaders headers = new HttpHeaders();
9695
headers.add("X-Forwarded-Prefix", "/prefix");
9796
ServerHttpRequest request = MockServerHttpRequest
@@ -107,7 +106,7 @@ public void xForwardedPrefixShouldNotLeadToDecodedPath() throws Exception {
107106
}
108107

109108
@Test
110-
public void xForwardedPrefixTrailingSlash() throws Exception {
109+
void xForwardedPrefixTrailingSlash() throws Exception {
111110
HttpHeaders headers = new HttpHeaders();
112111
headers.add("X-Forwarded-Prefix", "/prefix////");
113112
ServerHttpRequest request = this.requestMutator.apply(getRequest(headers));
@@ -118,7 +117,7 @@ public void xForwardedPrefixTrailingSlash() throws Exception {
118117
}
119118

120119
@Test // SPR-17525
121-
public void shouldNotDoubleEncode() throws Exception {
120+
void shouldNotDoubleEncode() throws Exception {
122121
HttpHeaders headers = new HttpHeaders();
123122
headers.add("Forwarded", "host=84.198.58.199;proto=https");
124123

@@ -133,6 +132,28 @@ public void shouldNotDoubleEncode() throws Exception {
133132
assertForwardedHeadersRemoved(request);
134133
}
135134

135+
@Test
136+
void shouldConcatenatePrefixes() throws Exception {
137+
HttpHeaders headers = new HttpHeaders();
138+
headers.add("X-Forwarded-Prefix", "/first,/second");
139+
ServerHttpRequest request = this.requestMutator.apply(getRequest(headers));
140+
141+
assertThat(request.getURI()).isEqualTo(new URI("https://example.com/first/second/path"));
142+
assertThat(request.getPath().value()).isEqualTo("/first/second/path");
143+
assertForwardedHeadersRemoved(request);
144+
}
145+
146+
@Test
147+
void shouldConcatenatePrefixesWithTrailingSlashes() throws Exception {
148+
HttpHeaders headers = new HttpHeaders();
149+
headers.add("X-Forwarded-Prefix", "/first/,/second//");
150+
ServerHttpRequest request = this.requestMutator.apply(getRequest(headers));
151+
152+
assertThat(request.getURI()).isEqualTo(new URI("https://example.com/first/second/path"));
153+
assertThat(request.getPath().value()).isEqualTo("/first/second/path");
154+
assertForwardedHeadersRemoved(request);
155+
}
156+
136157

137158
private MockServerHttpRequest getRequest(HttpHeaders headers) {
138159
return MockServerHttpRequest.get(BASE_URL).headers(headers).build();

0 commit comments

Comments
 (0)