Hi,
We have a Spring (5.3.3) application with a SockJS setup, deployed as follows:
[Browser / SockJS client] -> [Traefik load balancer] -> [Spring / Undertow / SockJS Server]
With this setup, Spring rejects the WebSocket upgrade requests as violating the Single Origin Policy. Here is my understanding of how it happens.
- The browser / SockJS client issues the request to the load balancer
with an url akin:
wss://my-website.com/api/notifications/008/sispjx3j/websocket
The Origin header is set to:Origin: https://my-website.com
. - The load balancer forwards the request to the Spring application,
and doing so adds the following request headers:
X-Forwarded-Host: my-website.com
X-Forwarded-Port: 443
X-Forwarded-Proto: wss
- Undertow's
ProxyPeerAddressHandler
class (enabled by the Spring Boot autoconfiguration) reads theX-Forwarded-*
headers and applies them to the request to make it as if the browser requested the Spring application directly. - Spring's
DefaultCorsProcessor
(enabled by the SockJS configuration) notices theOrigin
header in the request and validates it. The request is misidentified as cross origin because the request scheme in the Origin header ishttps
while the request scheme inHttpServletRequest
iswss
. The request is rejected because as the application is configured to only allow same origin requests, the set of allowed origins is empty.
It is not clear to me which component is at fault with this issue.
Comment From: rstoyanchev
It looks to me like it's the Origin header that's the odd one out and doesn't match the protocol of the URL used. We could consider relaxing the CORS checks to treat "wss" as a match for an Origin header with "https". @sdeleuze, @rwinch, what do you think?
Comment From: sdeleuze
We could maybe indeed potentially relax the scheme comparison in org.springframework.web.cors.CorsUtils#isCorsRequest
to consider as equivalent ws
with http
and wss
with https
given how the WebSocket specification qualify the protocol change here:
This change of scheme is essential to integrate well with fetching. E.g., HSTS would not work without it. There is no real reason for WebSocket to have distinct schemes, it’s a legacy artefact.
That said, I am not sure to fully undertand if this issue is specific to the usage of a load balancer, and why. If the ProxyPeerAddressHandler
class "applies the X-Forwarded-*
headers to the request to make it as if the browser requested the Spring application directly", what triggers this issue that, I think, we don't see for regular use case without such load balancer?
Comment From: bgK
That said, I am not sure to fully undertand if this issue is specific to the usage of a load balancer, and why. If the
ProxyPeerAddressHandler
class "applies theX-Forwarded-*
headers to the request to make it as if the browser requested the Spring application directly", what triggers this issue that, I think, we don't see for regular use case without such load balancer?
The scheme the browser used is not part of the HTTP request message. It's up to the server to infer it. In this case:
* The Traefik load balancer uses wss
when setting the X-Forwarded-Proto
header.
* The Undertow server decides it is https
for HttpServletRequest
when called directly.
Comment From: rstoyanchev
Traefik sets X-Forwarded-Proto to ws
or wss
as long as WebSocket upgrade headers are present, i.e. irrespective of the protocol of the underlying request. There is even a TODO in the source code whether to use the protocol of the underlying request, but the position taken in https://github.com/traefik/traefik/issues/6388 is that wss
is a registered protocol and should work, which is fine but I wonder if they consider the resulting mismatch between the protocols in the Origin
and the X-Forwarded-Proto
headers something worth addressing one way or another?
Comment From: rstoyanchev
@bgK, since you have the original scenario and setup, and in case of follow-up questions, would you consider creating an issue on https://github.com/traefik/traefik? Essentially, as per the answers under https://github.com/traefik/traefik/issues/6388, "wss" is indeed a valid protocol, but setting "X-Forwarded-Proto" in this way can leave it misaligned with the "Origin" header. Should Traefik ensure the two are in sync with each other one way or another?
We can still enhance our checks in DefaultCorsProcessor
to treat "wss" as a match "https", but a change at the level of Traefik would be more broadly applicable.
Comment From: sdeleuze
@rstoyanchev I am also in favor of creating such issue on https://github.com/traefik/traefik side, and I think I would wait for the feedback before doing anything on Spring side because while I could be ok with modification limited to CorsUtils#isCorsRequest
, changing the CORS origin comparison in DefaultCorsProcessor
or CorsConfiguration
seems going too far and should be motivated with very good and specific reasons, and for now it sounds like implementing a behavior not complying with https://www.w3.org/TR/cors/ specification to workaround a potential Traefik bug.
Maybe we should mark this issue as blocked
and update it when the issue is created on Traefik side?
Comment From: bgK
@bgK, since you have the original scenario and setup, and in case of follow-up questions, would you consider creating an issue on https://github.com/traefik/traefik?
Sure, I've submitted https://github.com/traefik/traefik/issues/9736.
Comment From: sdeleuze
Given the discussion in https://github.com/traefik/traefik/issues/9736 where a workaround is mentioned with traefik V2, and a potential related change is traefik V3, with no actionable item identified on Spring side, I close this issue.