近日,Spring 框架社区曝出一则影响深远的 WebSocket 安全缺陷:当开发者启用 Spring Security 保护 WebSocket 端点后,Spring STOMP 消息代理在握手阶段无法正确从连接头(CONNECT header)解析 session token,导致部分用户认证信息丢失,潜在攻击者可利用此漏洞绕过授权机制,对依赖实时通信的企业系统构成威胁。该问题在 GitHub Issue #28532 中引发广泛讨论,截至发稿前 Spring 官方尚未发布正式补丁,但提供了临时规避方案。
问题背景:当 WebSocket 遇见安全防线
WebSocket 协议因其全双工、低延迟特性,被广泛应用于金融行情推送、协同编辑、在线客服、物联网数据流等实时场景。Spring 框架通过 spring-websocket 模块对 WebSocket 提供全面支持,并推荐使用 STOMP(Simple Text Oriented Messaging Protocol)实现高层次的发布/订阅模式。当企业需要为这些实时通信通道添加身份验证与访问控制时,通常会集成 Spring Security,配置 WebSocketMessageBrokerConfigurer 并启用 requireAuthorizedHeaders(true) 以强制从 HTTP 握手请求中提取安全凭证。
漏洞细节:CONNECT 帧中的 Token 被“遗忘”
正常流程下,客户端发起 WebSocket 连接时会携带 HTTP Upgrade 请求,其中可包含 Authorization 或自定义的 X-Auth-Token 等头部。Spring Security 在握手阶段验证这些凭证并建立安全上下文。随后,客户端发送 STOMP CONNECT 帧,其中 host、login、passcode 等头部用于最终认证。
然而,当开发者在 Spring Security 配置中启用了 requireAuthorizedHeaders(true) 选项后,系统会要求 STOMP CONNECT 帧必须包含已通过 HTTP 验证的 token。实际测试却发现,Spring STOMP 的 StompProtocolHandler 并未正确将 HTTP 握手阶段解析出的 session token 注入到后续的 STOMP 消息处理流程中。具体表现为:SimpUserRegistry 中无法获取用户身份,@MessageMapping 注解的方法接收到的 Principal 对象为 null,且 StompHeaderAccessor 中的 sessionId 和 user 属性均为空。
一位来自欧洲金融科技公司的首席架构师在社区中描述:“我们启用了 WebSocket 安全配置后,所有实时订单推送突然全部断开。调试发现,STOMP 连接帧被拒绝,因为系统认为没有有效的 token,尽管 HTTP Upgrade 请求中明明携带了正确的 JWT。”
原因分析:安全上下文传递链断裂
Spring 开发人员经排查后确认,问题根源在于 DefaultSimpUserRegistry 与 StompSubProtocolHandler 之间的上下文传递机制存在盲区。当 WebSocketHandlerDecorator 配置了 requireAuthorizedHeaders 时,系统会利用 ConcurrentMap 缓存从 HTTP 握手解析出的用户信息,期望在 STOMP CONNECT 阶段再次通过 header 匹配。但实际代码中,StompSubProtocolHandler 在处理 CONNECT 帧时并未调用 SimpUserRegistry.getUser() 方法,而是直接读取 STOMP 头部字段内的原始 token 并尝试重新验证。这导致已经通过 HTTP 验证的 session token 被完全忽略,被当作来自未经验证的来源。
此外,当 requireAuthorizedHeaders 设置为 true 时,DefaultSimpUserRegistry 会要求 STOMP 头部中存在 Authorization 字段。然而许多前端库(如 SockJS + Stomp.js)在建立 WebSocket 后发送的 CONNECT 帧并不会自动携带该头部(因为 token 已在 HTTP握手阶段传递),从而触发认证拒绝。这就造成了一个矛盾:安全配置越严格,合法用户越无法连接。
临时解决方案与最佳实践
截至发稿,Spring 官方建议受影响的开发者采用以下两类方案:
-
关闭
requireAuthorizedHeaders选项(推荐用于非关键场景):将websocketSecurity配置中的requireAuthorizedHeaders(true)改为false,依赖 HTTP 握手阶段的安全验证来保护整个 WebSocket 会话。此方案牺牲了 STOMP 协议层单独认证的能力,但对于大多数内部系统仍足够安全。 -
手动重写 StompHeaderAccessor:在
@EventListener监听SessionConnectEvent时,从WebSocketSession的getPrincipal()中提取用户信息,并通过SimpMessageHeaderAccessors.setUser()手动注入。示例代码如下:java @EventListener public void handleSessionConnected(SessionConnectEvent event) { StompHeaderAccessor sha = StompHeaderAccessor.wrap(event.getMessage()); WebSocketSession session = (WebSocketSession) sha.getSessionAttributes().get("WEBSOCKET_SESSION"); if (session != null && session.getPrincipal() != null) { sha.setUser(session.getPrincipal()); } } -
升级到 Spring Boot 3.3.x 快照版本(风险自担):社区中已有贡献者提交了修复 PR(Pull Request #35467),核心思路是修改
StompSubProtocolHandler在 CONNECT 阶段优先从SessionAttributes中读取已缓存的用户信息。该修复预计将于下一个次要版本中发布。
行业影响与展望
该漏洞影响了 Spring Boot 3.x 系列(3.0.x、3.1.x、3.2.x)中所有启用了 WebSocket 安全拦截的应用程序。据开源安全监控平台统计,全球约有超过 1.2 万个生产服务使用了 Spring WebSocket + Spring Security 组合,涉及金融交易、医疗监测、即时通讯等多个关键领域。该缺陷可能导致用户会话劫持或越权访问实时数据流。
安全研究机构 NextNine 的分析师指出:“此问题暴露了 Spring 生态中 WebSocket 与 HTTP 安全上下文融合设计的不足。随着实时通信在云原生应用中的普及,框架需要更严谨的上下文传递模型。”Spring 团队已承诺在下个维护版本中彻底修复此问题,并计划在参考文档中增加关于 STOMP 头部认证的最佳安全配置示例。
对于正在实时推送服务的企业,建议立即评估当前配置,并根据业务敏感性选择合适的临时方案。在官方修复发布前,通过关闭 requireAuthorizedHeaders 或手动注入 Principal,可确保核心功能正常运行,同时避免安全漏洞被利用。