Spring's ForwardedHeaderFilter
handles the header value X-Forwarded-Proto: wss
incorrectly. The class has hardcoded checks for "http" and "https", which is in line with the widely held assumption that these are the only valid values (see e.g. MDN and other sources when searching for the header name on the web).
But X-Forwarded-Proto
is not standardized, and at least one popular proxy, Traefik, has decided to use "ws" and "wss" in the header, see https://github.com/traefik/traefik/issues/6388. We have run into problems with Spring because of that. Also Tomcat has added "ws"/"wss" as supported values: https://github.com/apache/tomcat/pull/311
I suggest that "wss" should be a supported value and handled consistently like "https".
ForwardedHeaderFilter
and its dependency UriComponentsBuilder.fromHttpRequest(request)
have hardcoded checks for "http" and "https" in multiple places ([1], [2]). Depending on which other X-Forwarded-*
headers are present, it maps a request on port 443 and X-Forwarded-Proto: wss
to the following ServletRequest
properties: scheme=wss, port=80, secure=false
or scheme=wss, port=443, secure=false
. This seems clearly incorrect.
Based on my observations I would propose that X-Forwarded-Proto: wss
gets mapped into the ServletRequest.scheme
value "https" (and "ws" -> "http") because that seems the most compatible way. Leaving "wss" in the ServletRequest.scheme
seems to lead to problems. A same-origin check that compares the value of the Origin
header (where "https" would be the protocol) with the servlet request url would fail. I also know of at least one library that fails completely when it encounters scheme=wss
in the ServletRequest
object.
Comment From: bclozel
@jhyot Thanks for raising this issue.
At least one of the Spring Framework code snippets you're pointing at is outdated. I'm not sure I completely understand the issue here. We do have a couple of places with "hardcoded" https values, but we seem to cover the scheme part dynamically, without restricting it to "
Could you produce a code snippet or a sample application that shows the wrong behavior here?
The comment you're pointing to seems to indicate that Spring's filter did set the scheme as "wss" and that Tomcat didn't support that back then. Now the Tomcat handler supports it properly so I'm wondering what needs to be fixed on our side.
Depending on which other X-Forwarded-* headers are present, it maps a request on port 443 and X-Forwarded-Proto: wss to the following ServletRequest properties: scheme=wss, port=80, secure=false or scheme=wss, port=443, secure=false. This seems clearly incorrect.
I've written the following test and it's green; am I missing something here?
void wssForwardedProtocol() {
MockHttpServletRequest request = new MockHttpServletRequest();
request.addHeader("X-Forwarded-Proto", "wss");
request.addHeader("X-Forwarded-Host", "84.198.58.199");
request.addHeader("X-Forwarded-Port", 443);
request.setScheme("http");
request.setServerName("example.com");
request.setServerPort(80);
request.setRequestURI("/rest/mobile/users/1");
HttpRequest httpRequest = new ServletServerHttpRequest(request);
UriComponents result = UriComponentsBuilder.fromHttpRequest(httpRequest).build();
assertThat(result.getScheme()).isEqualTo("wss");
assertThat(result.getHost()).isEqualTo("84.198.58.199");
assertThat(result.getPort()).isEqualTo(-1);
assertThat(result.getPath()).isEqualTo("/rest/mobile/users/1");
}
Based on my observations I would propose that X-Forwarded-Proto: wss gets mapped into the ServletRequest.scheme value "https" (and "ws" -> "http") because that seems the most compatible way. Leaving "wss" in the ServletRequest.scheme seems to lead to problems. A same-origin check that compares the value of the Origin header (where "https" would be the protocol) with the servlet request url would fail. I also know of at least one library that fails completely when it encounters scheme=wss in the ServletRequest object.
With that last section, it seems we're tackling a different issue; from supporting "wss" as a protocol value, to somehow translating it differently in the resulting request. Wouldn't that behavior break the expected behavior of the forwarded headers? Could you be more specific and explain what library fails and how?
Comment From: danielwegener
I think the main issue exists at
https://github.com/spring-projects/spring-framework/blob/main/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java#L242 (today in main, maybe in its reactive counterpart as well)
this.secure = "https".equals(this.scheme);
with a clearly unexpected result for the scheme value "wss".
Comment From: rstoyanchev
I've written the following test and it's green; am I missing something here?
This fails for me for the port which is 443 because this check for https && 443 doesn't null it out.
Also this test fails because the secure
flag of the request is not set to true:
@Test
public void forwardedRequestWss() throws Exception {
this.request.setRequestURI("/path");
this.request.addHeader(X_FORWARDED_PROTO, "wss");
this.request.addHeader(X_FORWARDED_HOST, "84.198.58.199");
this.filter.doFilter(this.request, new MockHttpServletResponse(), this.filterChain);
HttpServletRequest actual = (HttpServletRequest) this.filterChain.getRequest();
assertThat(actual.isSecure()).isTrue();
}
Comment From: jhyot
@rstoyanchev Thank you, that is what I mean. You can also add
assertThat(actual.getServerPort()).isEqualTo(443);
to your test, since that currently fails as well (as it is coupled to the isSecure
value -> see https://github.com/spring-projects/spring-framework/blob/171b283e49ca995c457a2f31f3bd6add9cfa7443/spring-web/src/main/java/org/springframework/web/filter/ForwardedHeaderFilter.java#L244 )
@bclozel
With that last section, it seems we're tackling a different issue; from supporting "wss" as a protocol value, to somehow translating it differently in the resulting request. Wouldn't that behavior break the expected behavior of the forwarded headers? Could you be more specific and explain what library fails and how?
Yes I guess that can be seen as a separate issue. Should I open a new ticket to discuss why possibly translating "wss" to "https" might make sense? Currently, since "wss" is kept as the protocol value in the Servlet Request, spring-websocket fails during the same-origin check (as it checks for the protocol as well, and the Origin header has the value "https://..."). And "com.microsoft.azure:applicationinsights-web" at least in 2.x throws an Exception when encountering wss (we haven't upgraded to the newest 3.x, so can't test that yet).
Comment From: rstoyanchev
These should be fixed now.
As for translating "wss" to "https", that's not straight forward and as Brian said, probably not something we would want to change. Our CORS checks do take into account "wss" though so that should work.