Skip to content

Commit 895fa2e

Browse files
committed
Add removeOnly mode to ForwardedHeaderFilter
Issue: SPR-15610
1 parent cf1bc81 commit 895fa2e

File tree

2 files changed

+115
-50
lines changed

2 files changed

+115
-50
lines changed

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

+87-49
Original file line numberDiff line numberDiff line change
@@ -40,20 +40,25 @@
4040
import org.springframework.web.util.UrlPathHelper;
4141

4242
/**
43-
* Filter that wraps the request and response in order to override its
43+
* Extract values from "Forwarded" and "X-Forwarded-*" headers in order to wrap
44+
* and override the following from the request and response:
4445
* {@link HttpServletRequest#getServerName() getServerName()},
4546
* {@link HttpServletRequest#getServerPort() getServerPort()},
4647
* {@link HttpServletRequest#getScheme() getScheme()},
47-
* {@link HttpServletRequest#isSecure() isSecure()},
48-
* {@link HttpServletResponse#sendRedirect(String) sendRedirect(String)},
49-
* methods with values derived from "Forwarded" or "X-Forwarded-*"
50-
* headers. In effect the wrapped request and response reflects the
51-
* client-originated protocol and address.
48+
* {@link HttpServletRequest#isSecure() isSecure()}, and
49+
* {@link HttpServletResponse#sendRedirect(String) sendRedirect(String)}.
50+
* In effect the wrapped request and response reflect the client-originated
51+
* protocol and address.
52+
*
53+
* <p><strong>Note:</strong> This filter can also be used in a
54+
* {@link #setRemoveOnly removeOnly} mode where "Forwarded" and "X-Forwarded-*"
55+
* headers are only eliminated without being used.
5256
*
5357
* @author Rossen Stoyanchev
5458
* @author Eddú Meléndez
5559
* @author Rob Winch
5660
* @since 4.3
61+
* @see <a href="https://tools.ietf.org/html/rfc7239">https://tools.ietf.org/html/rfc7239</a>
5762
*/
5863
public class ForwardedHeaderFilter extends OncePerRequestFilter {
5964

@@ -71,6 +76,8 @@ public class ForwardedHeaderFilter extends OncePerRequestFilter {
7176

7277
private final UrlPathHelper pathHelper;
7378

79+
private boolean removeOnly;
80+
7481

7582
public ForwardedHeaderFilter() {
7683
this.pathHelper = new UrlPathHelper();
@@ -79,6 +86,17 @@ public ForwardedHeaderFilter() {
7986
}
8087

8188

89+
/**
90+
* Enables mode in which any "Forwarded" or "X-Forwarded-*" headers are
91+
* removed only and the information in them ignored.
92+
* @param removeOnly whether to discard and ingore forwarded headers
93+
* @since 4.3.9
94+
*/
95+
public void setRemoveOnly(boolean removeOnly) {
96+
this.removeOnly = removeOnly;
97+
}
98+
99+
82100
@Override
83101
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
84102
Enumeration<String> names = request.getHeaderNames();
@@ -105,13 +123,67 @@ protected boolean shouldNotFilterErrorDispatch() {
105123
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
106124
FilterChain filterChain) throws ServletException, IOException {
107125

108-
ForwardedHeaderRequestWrapper wrappedRequest = new ForwardedHeaderRequestWrapper(request, this.pathHelper);
109-
ForwardedHeaderResponseWrapper wrappedResponse = new ForwardedHeaderResponseWrapper(response, wrappedRequest);
110-
filterChain.doFilter(wrappedRequest, wrappedResponse);
126+
if (this.removeOnly) {
127+
ForwardedHeaderRemovingRequest theRequest = new ForwardedHeaderRemovingRequest(request);
128+
filterChain.doFilter(theRequest, response);
129+
}
130+
else {
131+
HttpServletRequest theRequest = new ForwardedHeaderExtractingRequest(request, this.pathHelper);
132+
HttpServletResponse theResponse = new ForwardedHeaderExtractingResponse(response, theRequest);
133+
filterChain.doFilter(theRequest, theResponse);
134+
}
111135
}
112136

113137

114-
private static class ForwardedHeaderRequestWrapper extends HttpServletRequestWrapper {
138+
/**
139+
* Hide "Forwarded" or "X-Forwarded-*" headers.
140+
*/
141+
private static class ForwardedHeaderRemovingRequest extends HttpServletRequestWrapper {
142+
143+
private final Map<String, List<String>> headers;
144+
145+
146+
public ForwardedHeaderRemovingRequest(HttpServletRequest request) {
147+
super(request);
148+
this.headers = initHeaders(request);
149+
}
150+
151+
private static Map<String, List<String>> initHeaders(HttpServletRequest request) {
152+
Map<String, List<String>> headers = new LinkedCaseInsensitiveMap<>(Locale.ENGLISH);
153+
Enumeration<String> names = request.getHeaderNames();
154+
while (names.hasMoreElements()) {
155+
String name = names.nextElement();
156+
if (!FORWARDED_HEADER_NAMES.contains(name)) {
157+
headers.put(name, Collections.list(request.getHeaders(name)));
158+
}
159+
}
160+
return headers;
161+
}
162+
163+
// Override header accessors to not expose forwarded headers
164+
165+
@Override
166+
public String getHeader(String name) {
167+
List<String> value = this.headers.get(name);
168+
return (CollectionUtils.isEmpty(value) ? null : value.get(0));
169+
}
170+
171+
@Override
172+
public Enumeration<String> getHeaders(String name) {
173+
List<String> value = this.headers.get(name);
174+
return (Collections.enumeration(value != null ? value : Collections.emptySet()));
175+
}
176+
177+
@Override
178+
public Enumeration<String> getHeaderNames() {
179+
return Collections.enumeration(this.headers.keySet());
180+
}
181+
}
182+
183+
/**
184+
* Extract and use "Forwarded" or "X-Forwarded-*" headers.
185+
*/
186+
private static class ForwardedHeaderExtractingRequest extends ForwardedHeaderRemovingRequest {
115187

116188
private final String scheme;
117189

@@ -127,9 +199,8 @@ private static class ForwardedHeaderRequestWrapper extends HttpServletRequestWra
127199

128200
private final String requestUrl;
129201

130-
private final Map<String, List<String>> headers;
131202

132-
public ForwardedHeaderRequestWrapper(HttpServletRequest request, UrlPathHelper pathHelper) {
203+
public ForwardedHeaderExtractingRequest(HttpServletRequest request, UrlPathHelper pathHelper) {
133204
super(request);
134205

135206
HttpRequest httpRequest = new ServletServerHttpRequest(request);
@@ -145,7 +216,6 @@ public ForwardedHeaderRequestWrapper(HttpServletRequest request, UrlPathHelper p
145216
this.contextPath = (prefix != null ? prefix : request.getContextPath());
146217
this.requestUri = this.contextPath + pathHelper.getPathWithinApplication(request);
147218
this.requestUrl = this.scheme + "://" + this.host + (port == -1 ? "" : ":" + port) + this.requestUri;
148-
this.headers = initHeaders(request);
149219
}
150220

151221
private static String getForwardedPrefix(HttpServletRequest request) {
@@ -165,21 +235,6 @@ private static String getForwardedPrefix(HttpServletRequest request) {
165235
return prefix;
166236
}
167237

168-
/**
169-
* Copy the headers excluding any {@link #FORWARDED_HEADER_NAMES}.
170-
*/
171-
private static Map<String, List<String>> initHeaders(HttpServletRequest request) {
172-
Map<String, List<String>> headers = new LinkedCaseInsensitiveMap<>(Locale.ENGLISH);
173-
Enumeration<String> names = request.getHeaderNames();
174-
while (names.hasMoreElements()) {
175-
String name = names.nextElement();
176-
if (!FORWARDED_HEADER_NAMES.contains(name)) {
177-
headers.put(name, Collections.list(request.getHeaders(name)));
178-
}
179-
}
180-
return headers;
181-
}
182-
183238
@Override
184239
public String getScheme() {
185240
return this.scheme;
@@ -214,35 +269,18 @@ public String getRequestURI() {
214269
public StringBuffer getRequestURL() {
215270
return new StringBuffer(this.requestUrl);
216271
}
217-
218-
// Override header accessors to not expose forwarded headers
219-
220-
@Override
221-
public String getHeader(String name) {
222-
List<String> value = this.headers.get(name);
223-
return (CollectionUtils.isEmpty(value) ? null : value.get(0));
224-
}
225-
226-
@Override
227-
public Enumeration<String> getHeaders(String name) {
228-
List<String> value = this.headers.get(name);
229-
return (Collections.enumeration(value != null ? value : Collections.emptySet()));
230-
}
231-
232-
@Override
233-
public Enumeration<String> getHeaderNames() {
234-
return Collections.enumeration(this.headers.keySet());
235-
}
236272
}
237273

238274

239-
private static class ForwardedHeaderResponseWrapper extends HttpServletResponseWrapper {
275+
private static class ForwardedHeaderExtractingResponse extends HttpServletResponseWrapper {
240276

241277
private static final String FOLDER_SEPARATOR = "/";
242278

279+
243280
private final HttpServletRequest request;
244281

245-
public ForwardedHeaderResponseWrapper(HttpServletResponse response, HttpServletRequest request) {
282+
283+
public ForwardedHeaderExtractingResponse(HttpServletResponse response, HttpServletRequest request) {
246284
super(response);
247285
this.request = request;
248286
}

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

+28-1
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@
3232
import org.springframework.mock.web.test.MockHttpServletRequest;
3333
import org.springframework.mock.web.test.MockHttpServletResponse;
3434

35-
import static org.junit.Assert.*;
35+
import static org.junit.Assert.assertEquals;
36+
import static org.junit.Assert.assertFalse;
37+
import static org.junit.Assert.assertNull;
38+
import static org.junit.Assert.assertTrue;
3639

3740
/**
3841
* Unit tests for {@link ForwardedHeaderFilter}.
@@ -239,6 +242,30 @@ public void forwardedRequest() throws Exception {
239242
assertEquals("bar", actual.getHeader("foo"));
240243
}
241244

245+
@Test
246+
public void forwardedRequestInRemoveOnlyMode() throws Exception {
247+
this.request.setRequestURI("/mvc-showcase");
248+
this.request.addHeader(X_FORWARDED_PROTO, "https");
249+
this.request.addHeader(X_FORWARDED_HOST, "84.198.58.199");
250+
this.request.addHeader(X_FORWARDED_PORT, "443");
251+
this.request.addHeader("foo", "bar");
252+
253+
this.filter.setRemoveOnly(true);
254+
this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain);
255+
HttpServletRequest actual = (HttpServletRequest) this.filterChain.getRequest();
256+
257+
assertEquals("http://localhost/mvc-showcase", actual.getRequestURL().toString());
258+
assertEquals("http", actual.getScheme());
259+
assertEquals("localhost", actual.getServerName());
260+
assertEquals(80, actual.getServerPort());
261+
assertFalse(actual.isSecure());
262+
263+
assertNull(actual.getHeader(X_FORWARDED_PROTO));
264+
assertNull(actual.getHeader(X_FORWARDED_HOST));
265+
assertNull(actual.getHeader(X_FORWARDED_PORT));
266+
assertEquals("bar", actual.getHeader("foo"));
267+
}
268+
242269
@Test
243270
public void requestUriWithForwardedPrefix() throws Exception {
244271
this.request.addHeader(X_FORWARDED_PREFIX, "/prefix");

0 commit comments

Comments
 (0)