摘要

  • WebSocket 掩码不保密:四字节密钥随帧公开传输;它的价值在于每帧不可预测,使应用无法事先选定客户端到服务器方向的线上字节。
  • 这条规则源自真实的中间设备故障:客户端帧必须掩码,服务器帧不得掩码;一旦发送开始,应用也不得再改动该帧的载荷。

最初的帧只相信协议边界

IETF 的 2010 年 5 月草案 试图在一次 HTTP 开场之后,为浏览器建立双向通道。早期线格式很简洁:文本帧由 0x00 开始、以 0xff 结束,另一种帧则携带长度。只要两端都按 WebSocket 语法工作,这些边界足以分隔消息。

问题在于,路径上的每台设备未必承认协议已经切换。一个拦截式代理可能放行 Upgrade,随后却继续在新数据流里寻找下一条 HTTP 请求。端点已经进入 WebSocket,代理的解析器仍停留在旧世界。

这种危险不要求代理本身怀有恶意。浏览器和服务器也可能都遵守约定。只要中间设备把接下来的字节交给错误的语法,原本合法的通信就可能产生双方都未授权的缓存副作用。

固定变换仍把选择权留给脚本

到了 2011 年 1 月,-04 草案 已要求客户端对发往服务器的每个帧做掩码。然而,密钥由握手值导出,并在整条连接上保持不变。

稳定的变换会改变数据外观,却不一定改变发送者对结果的控制。若脚本能推知变换规则,它就能反向选择输入,得到想要的输出。表面上经过“打乱”的字节,仍然可以被攻击者精确安排。

真正需要的约束更窄也更强:对每一个帧,应用可以决定逻辑消息,也可以在事后看到变换,但不能在发送之前同时掌握消息和下一次变换,从而挑选最终的线上图像。

一台代理给响应贴错了名字

2011 年的论文 Talking to Yourself for Fun and Profit 研究了浏览器套接字机制穿过透明代理——更准确说是拦截式代理——时的行为。部分设备会转发同意或升级流量,却没有真正理解状态变化,之后又把攻击者可控的数据当作 HTTP 请求。

攻击跨越了多方的权限边界。恶意站点让浏览器连接攻击者的服务器;迷惑的代理把流中的某段字节解释为另一资源的请求;攻击服务器再提供像响应一样的数据;共享缓存最后把这份内容存到错误的资源名下,后续用户便可能收到并非来自该名称所指源站的内容。

研究者在 2011 年 3 月的广告实验中,在少量但非零的 Java 和 Flash 路径上观察到缓存投毒条件;在 47,338 次抵达测试点的、基于 Upgrade 的 WebSocket 原型握手中,有 8 次成功。这个数字只描述当时的样本,不能当作今天代理部署的比例。

论文留下的长期结论并不是某个百分比。给载荷加一段“看起来不像 HTTP”的前缀,无法证明未知的错误解析器不会跳过它。更可靠的做法,是让恶意应用无法主动构造中间设备最危险的那组线上字节。

密钥进入每一个帧

2011 年 2 月的 -05 草案 作出了决定性的调整:每个客户端帧携带自己的 32 位密钥,密钥必须来自强熵源,而且不能由先前的值预测下一值。RFC 4086 解释了原因:时钟、计数器或过小种子产生的序列,即使统计外观杂乱,也可能被对手预测。

最终的 RFC 6455 保留了逐帧方案。MASK 位声明是否存在四个密钥字节;载荷字节 i 与密钥字节 i mod 4 做异或。载荷长度不包含密钥,变换也不改变载荷长度。

密钥公开不是缺陷,而是设计的一部分。服务器必须取得它才能恢复消息,路径上的观察者同样可以恢复。安全性质来自决定的先后顺序:应用先交付内容,客户端再用应用无法预知的密钥确定线上表示。保密解决的是另一类问题。

发送开始,帧就不能再改

仅有新密钥还不够。假如应用可以用长帧开头的已知明文推算重复的四字节变换,再改写尚未发出的尾部,它仍能让尾部经掩码后呈现为 HTTP 请求。

RFC 6455 因此规定了时间边界:客户端帧一旦开始传输,应用就不能继续修改该帧的载荷。新增或变更的数据必须进入另一个帧,并获得另一把新密钥。

这使“随机”与“承诺”成为同一条规则。发送前,应用掌握内容;发送开始后,客户端实现保管一条已经固定的字节序列。若没有这次权力交接,观察前缀就可能重新获得控制后缀的机会。

方向本身表达威胁模型

RFC 6455 要求所有客户端到服务器的帧都带掩码,同时禁止服务器给发往客户端的帧加掩码。服务器遇到未掩码的客户端帧必须关闭连接;客户端遇到掩码的服务器帧也必须关闭,可以使用协议错误码 1002。

这种不对称正好对应攻击链。缓存投毒需要浏览器朝服务器方向发出“像请求”的字节,才能给随后出现的响应安上虚假的资源身份。恶意服务器本就能选择“像响应”的数据,但缺少前面的伪请求,错误缓存还无法完成这次身份绑定。

这并不证明服务器数据可信。身份认证、授权、Origin 策略、内容校验和信道保护仍各有职责。只是对这个特定基础设施故障而言,反方向掩码并不是缺失的控制。

TLS 没有让掩码失去意义

无论使用 ws 还是置于 TLS 中的 wss,客户端掩码都适用。加密路径上的设备通常看不到正确保护的字节,于是这条规则似乎显得多余;但两者声明的性质不同。TLS 保护端点之间的机密性与完整性,掩码则限制不可信脚本能让合规客户端发出什么样的帧载荷。

保持统一帧规则,也避免安全性取决于 TLS 在哪里终止。网关可能先解密,再把数据交给内部链路。调试和部署路径也会变化。一个传输层的保护,不应暗中改写另一个协议层的有效语法。

新的 HTTP 承载方式保留了旧约束

RFC 8441 后来使用 HTTP/2 流上的扩展 CONNECT 启动 WebSocket。由于 :protocol 已提供转换信号,这一路径不再处理 HTTP/1.1 的 Sec-WebSocket-Key 与 Sec-WebSocket-Accept。但它没有废除帧掩码:除与握手 SHA-1 相关的 10.8 节外,RFC 6455 第 10 节的安全考虑继续有效。

RFC 9220 把扩展 CONNECT 带到 HTTP/3,也没有增加新的安全例外。外层启动方式从整条 HTTP/1.1 连接上的协议切换,变成 HTTP/2 的选定流,再变成 QUIC 流;WebSocket 帧仍保留同样的方向性证据。

这种连续性说明,掩码不是为某一个 Upgrade 头临时打的补丁。它记录的是应用选择、客户端承诺和中间设备误读之间的关系,而这组关系能跨越承载协议的变化。

公开密钥能证明什么,不能证明什么

新密钥不证明身份,也不表明 Origin 被接受、操作已授权或数据离开受保护信道后仍保持完整。抓包里出现密钥是正常现象;重复或可预测的密钥,才说明核心假设受损。

合规掩码也无法修复互联网上每一台代理。RFC 6455 明确指出,不合规的客户端或服务器仍可能使脆弱的中间设备暴露在同类攻击下。协议只收窄了合规浏览器路径能被诱导去做的事情,并没有取得所有缓存的控制权。

历史教训因而很克制:当旧基础设施可能用错误语法解析共享路径上的字节,新协议不能只宣布自己“已经切换”。它还可能需要限制不可信参与者能否故意把危险字节排列送上那条路径。

来源与证据边界

草案和 RFC 能确定设计演变与规范行为。论文报告的是一次有边界的 2011 年实验,不是当前漏洞或市场份额。上述资料均不能证明任何具名现代浏览器、代理、CDN 或网关的现状。掩码不是加密、完整性保护、端点认证,也不能证明缓存响应确实属于表面上的源站。