flawopen.com/Teardowns/cve-2026-71416-headroom-websocket-origin-hijacking
CVE-2026-71416:Headroom WebSocket Origin 劫持
Headroom 未验证 WebSocket Origin,访问的网页可通过可达代理转发 LLM 请求。请升级至 0.35.0 或更高版本。
接待员不查访客证件,就让任何人进入私人服务台。Origin 就像访客证件:代理必须先验证它,才能打开可使用已存 API 密钥的通道。即使访客能到达服务台,门卫也要确认证件上的来源在允许名单中,不能只因对方敲门就放行。
核心概念与专有名词
WebSocket- 由 HTTP Upgrade 请求启动的持久双向连接,可在两端持续交换消息。
Origin 请求头- 浏览器发送的页面协议、主机和端口,用于标识发起请求的页面。WebSocket 握手时服务器必须执行自己的信任策略;CORS 响应头不能替代此检查。
CSWSH- 跨站 WebSocket 劫持:不可信网页向可达服务发起 WebSocket,而该服务未验证连接来源。
环境凭据- 进程已可使用的秘密,例如环境变量中的 API 密钥,可为没有自带凭据的请求进行认证。
CWE-1385- WebSocket 缺少 Origin 验证:端点未检查通信来源是否获准。
框架映射
- 本指南所述弱点的 CWE 参考: CWE-1385
- OWASP GenAI 2025 — LLM06:2025 过度代理权限
根本原因剖析 (Root Cause)
Headroom 的 /v1/responses WebSocket 处理程序在检查握手中的 Origin 之前就接受了浏览器连接。随后代理可能将自身环境中的 OPENAI_API_KEY 添加到上游请求。浏览器同源策略本身并不会阻止 JavaScript 向另一个可达主机发起 WebSocket;浏览器通常会发送 Origin,服务器必须拒绝不符合策略的来源。因此,利用条件包括受害者浏览器能够访问存在漏洞的 Headroom 代理,以及代理持有有用凭据或集成。公告称被接受的连接可发出任意 LLM 请求。主机命令执行还取决于请求能否使用已配置的命令执行工具;缺少请求头检查本身不会自动导致主机 RCE。CNA 列出 0.35.0 之前版本为受影响版本,而 GitHub 公告包元数据列出 0.23.0。请升级至 0.35.0 或更高版本,并核实已安装版本。
攻击执行流程分解
代理持有上游权限
Headroom 进程可能在 OPENAI_API_KEY 中保存 OpenAI 密钥;浏览器客户端无需发送该密钥。
浏览器访问不可信网页
若浏览器可访问 Headroom WebSocket 端点,网页脚本便可尝试建立连接。浏览器会在握手中包含网页来源。
存在漏洞的处理程序未检查来源便接受连接
受影响处理程序会在未先拒绝不可信 Origin 的情况下执行 websocket.accept()。同源策略不是服务器端 WebSocket 授权检查。
代理转发已认证请求
Headroom 可从环境中补充缺失的上游 Authorization 头。攻击者控制的网页因此可能借助代理权限发起请求;影响取决于工具、凭据、网络可达性和用户交互。
修复在路由前拒绝不允许的来源
上游补丁在接受和转发会话前验证来源。浏览器来源默认限制为 loopback,可配置明确来源,并以 1008 策略违规代码关闭不允许的连接。
源代码对比:漏洞与安全实现
公告指出 handle_openai_responses_ws() 在 websocket.accept() 前缺少验证。上游提交 c632023 添加 _is_allowed_websocket_origin(),并在路由会话前拒绝不允许的来源;它兼容不带 Origin 的原生客户端,默认仅允许浏览器 loopback 来源,并支持通过 HEADROOM_WS_ORIGINS 或 HEADROOM_CORS_ORIGINS 配置来源。此简化代码突出变更的信任边界,未完整列出辅助函数。上游回归测试覆盖 Origin 缺失、loopback、远程、格式错误及显式配置。
源码摘录: headroom/proxy/handlers/openai.py
async def handle_openai_responses_ws(self, websocket: WebSocket) -> None:
# 易受攻击的流程:在验证来源之前接受浏览器会话。
await websocket.accept()
await self._relay_responses_session(websocket)
async def handle_openai_responses_ws(self, websocket: WebSocket) -> None:
ws_headers = dict(websocket.headers)
if not _is_allowed_websocket_origin(ws_headers):
logger.warning(
"event=websocket_origin_not_allowed origin=%r",
_header_get(ws_headers, "origin"),
)
await websocket.close(code=1008, reason="origin not allowed")
return
# 只有通过策略校验的浏览器会话才能进入上游路由。
await websocket.accept()
await self._relay_responses_session(websocket)
工程与系统安全加固清单
- ✓将 Headroom 升级到 0.35.0 或更高版本;检查已安装包和部署镜像版本,而不只看依赖锁定文件。
- ✓在调用
accept()或转发任何上游消息前验证 WebSocketOrigin;除非明确加入允许列表,否则拒绝非 loopback 来源。 - ✓仅在独立验证客户端类型可信时兼容缺少
Origin的原生客户端;浏览器没有来源头并不代表已授权。 - ✓持有凭据的代理不要将允许来源设为
*;精确列出可信来源并测试默认端口规范化。 - ✓尽可能将本地开发代理绑定到 loopback;若其他主机可访问,则添加认证或网络访问控制。
- ✓在可行时使用权限受限、有效期较短的上游密钥;不要向跨越浏览器边界的请求开放 shell 等高影响工具。
- ✓添加回归测试,确认远程 Origin 在上游路由前以策略违规关闭,同时验证 loopback、配置来源和获准原生客户端行为。
- ✓命令执行取决于已配置工具能力;Origin 缺陷本身不等于自动主机 RCE。