You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
We faced with an issue after updating to spring-boot 4.0.7. We have a workaround, but it might be useful to have it resolved for everyone.
Component: spring-websocket / SockJS — TransportHandlingSockJsService Affected version: 7.0.8 (regression vs 7.0.7) Related:#36681 ("Improve principal checks for SockJS session"), shipped in 7.0.8 alongside the predictable-session-id fix for CVE-2026-41838 (#36740)
Summary
#36681 added remote-address binding for anonymous SockJS sessions: when the session has no principal, a follow-up transport request must now come from the same remote address as the request that created the session. This is good hardening, but the comparison uses the full InetSocketAddress (host and port), which is never stable for SockJS's HTTP fallback transports — they use multiple short-lived connections — and is guaranteed to differ behind a reverse proxy. The result is that anonymous SockJS-over-HTTP stops working entirely: the xhr_streaming receive opens, but the xhr_send POST is rejected with 404 and the STOMP CONNECT is never delivered.
Server log (DEBUG):
DefaultSockJsService : Processing transport request: POST /app/ws/253/qyg40c00/xhr_send
DefaultSockJsService : The remote address for the session and the request do not match.
Root cause
The check added in 7.0.8 (TransportHandlingSockJsService#handleTransportRequest, in the else branch when session.getPrincipal() == null):
InetSocketAddressremoteAddress = session.getRemoteAddress();
if (remoteAddress != null && !remoteAddress.equals(request.getRemoteAddress())) {
logger.debug("The remote address for the session and the request do not match.");
response.setStatusCode(HttpStatus.NOT_FOUND);
return;
}
InetSocketAddress.equals compares host and port. But:
SockJS HTTP transports (xhr, xhr_streaming, eventsource, htmlfile) deliberately span multiple TCP connections per session — a long-lived receive plus separate xhr_send POSTs — each with its own ephemeral remote port. So even with a direct connection the ports differ across a session's requests.
Behind a reverse proxy / load balancer it's always different: the proxy opens a fresh upstream connection per request, and X-Forwarded-For (or Forwarded) conveys only the client IP, never a stable port. Neither Tomcat's RemoteIpValve (forward-headers-strategy=native) nor ForwardedHeaderFilter (framework) normalizes the remote port — ForwardedHeaderUtils.parseForwardedFor falls back to the original connection's port when the header has none.
So session.getRemoteAddress() and the xhr_send request's getRemoteAddress() never match, and every send is 404'd.
This specifically affects sessions that are anonymous at the HTTP layer — e.g. apps where authorization is carried in the STOMP CONNECT frame rather than on the SockJS HTTP requests (a common Spring STOMP-over-WebSocket setup). Sessions with an HTTP Principal take the username-comparison branch and are unaffected.
Steps to reproduce
Spring Boot 4.0.7 (spring-websocket 7.0.8); a @EnableWebSocketMessageBroker endpoint with .withSockJS(); the SockJS HTTP path permitted anonymously.
Connect with a client that uses a SockJS HTTP transport (e.g. WebSocketStompClient + SockJsClient configured with only RestTemplateXhrTransport), ideally through a reverse proxy that sets X-Forwarded-For.
Observe: the o open frame is received, then xhr_send returns 404 ("The remote address for the session and the request do not match"); the connection never completes.
Downgrading only spring-websocket to 7.0.7 restores correct behavior.
Expected
SockJS HTTP transports continue to work behind a proxy and across the multiple connections they inherently use, as in <= 7.0.7 — without weakening the session-binding intent of #36681.
Actual
xhr_send -> 404; STOMP CONNECT never delivered; the websocket appears dead.
Suggested fix
The session-binding goal is reasonable, but the remote port can't be part of it for HTTP transports. Options:
And/or make the binding honor forwarded headers / be configurable (e.g. opt-out, or a "trusted proxy" mode) so deployments behind a proxy aren't broken.
Workaround (how we bypassed it)
Until this is addressed, we register a small servlet filter scoped to the SockJS endpoint that pins the request's remote port to a constant while leaving the host untouched — reducing the 7.0.8 check to a stable host-based comparison and preserving the per-client-IP binding:
(Registered via a FilterRegistrationBean on the SockJS endpoint path. This keeps IP-level binding while removing the port comparison that HTTP transports can't satisfy.)
We faced with an issue after updating to spring-boot 4.0.7. We have a workaround, but it might be useful to have it resolved for everyone.
Component: spring-websocket / SockJS —
TransportHandlingSockJsServiceAffected version: 7.0.8 (regression vs 7.0.7)
Related: #36681 ("Improve principal checks for SockJS session"), shipped in 7.0.8 alongside the predictable-session-id fix for CVE-2026-41838 (#36740)
Summary
#36681 added remote-address binding for anonymous SockJS sessions: when the session has no principal, a follow-up transport request must now come from the same remote address as the request that created the session. This is good hardening, but the comparison uses the full
InetSocketAddress(host and port), which is never stable for SockJS's HTTP fallback transports — they use multiple short-lived connections — and is guaranteed to differ behind a reverse proxy. The result is that anonymous SockJS-over-HTTP stops working entirely: thexhr_streamingreceive opens, but thexhr_sendPOST is rejected with 404 and the STOMPCONNECTis never delivered.Server log (DEBUG):
Root cause
The check added in 7.0.8 (
TransportHandlingSockJsService#handleTransportRequest, in theelsebranch whensession.getPrincipal() == null):InetSocketAddress.equalscompares host and port. But:xhr,xhr_streaming,eventsource,htmlfile) deliberately span multiple TCP connections per session — a long-lived receive plus separatexhr_sendPOSTs — each with its own ephemeral remote port. So even with a direct connection the ports differ across a session's requests.X-Forwarded-For(orForwarded) conveys only the client IP, never a stable port. Neither Tomcat'sRemoteIpValve(forward-headers-strategy=native) norForwardedHeaderFilter(framework) normalizes the remote port —ForwardedHeaderUtils.parseForwardedForfalls back to the original connection's port when the header has none.So
session.getRemoteAddress()and thexhr_sendrequest'sgetRemoteAddress()never match, and every send is 404'd.This specifically affects sessions that are anonymous at the HTTP layer — e.g. apps where authorization is carried in the STOMP
CONNECTframe rather than on the SockJS HTTP requests (a common Spring STOMP-over-WebSocket setup). Sessions with an HTTPPrincipaltake the username-comparison branch and are unaffected.Steps to reproduce
@EnableWebSocketMessageBrokerendpoint with.withSockJS(); the SockJS HTTP path permitted anonymously.WebSocketStompClient+SockJsClientconfigured with onlyRestTemplateXhrTransport), ideally through a reverse proxy that setsX-Forwarded-For.oopen frame is received, thenxhr_sendreturns 404 ("The remote address for the session and the request do not match"); the connection never completes.Downgrading only spring-websocket to 7.0.7 restores correct behavior.
Expected
SockJS HTTP transports continue to work behind a proxy and across the multiple connections they inherently use, as in <= 7.0.7 — without weakening the session-binding intent of #36681.
Actual
xhr_send-> 404; STOMPCONNECTnever delivered; the websocket appears dead.Suggested fix
The session-binding goal is reasonable, but the remote port can't be part of it for HTTP transports. Options:
Workaround (how we bypassed it)
Until this is addressed, we register a small servlet filter scoped to the SockJS endpoint that pins the request's remote port to a constant while leaving the host untouched — reducing the 7.0.8 check to a stable host-based comparison and preserving the per-client-IP binding:
(Registered via a
FilterRegistrationBeanon the SockJS endpoint path. This keeps IP-level binding while removing the port comparison that HTTP transports can't satisfy.)