一、WebSocket 传递消息的核心 ------ 帧(Frame)格式
WebSocket 不像 HTTP 那样用 "请求 - 响应报文" 传输数据,而是将所有消息(文本 / 二进制)拆分成帧(Frame) 传输 ------ 哪怕是一条简单的 "你好",也会被包装成一个帧发送。
帧是 WebSocket 数据传输的最小单位,其格式遵循 RFC 6455 规范,结构如下(按字节顺序):
| 字段 | 位数 | 核心作用 |
|---|---|---|
| FIN | 1 | 标识是否为消息的最后一帧:1= 最后一帧(完整消息),0= 后续还有帧(分片) |
| RSV1/RSV2/RSV3 | 1*3 | 扩展位,默认全 0,仅启用扩展(如压缩)时使用 |
| Opcode | 4 | 帧类型:• 0x0:延续帧(分片消息的后续帧)• 0x1:文本帧(UTF-8 数据)• 0x2:二进制帧• 0x8:关闭帧• 0x9:PING 帧• 0xA:PONG 帧 |
| MASK | 1 | 标识数据是否被掩码:1= 客户端发送的帧必须掩码,0= 服务器发送的帧不能掩码 |
| Payload length | 7/7+16/7+64 | 负载数据长度:・0-125:直接表示长度・126:后续 2 字节表示长度・127:后续 8 字节表示长度 |
| Masking-key | 0/32 | 掩码密钥:仅 MASK=1 时存在(客户端帧必带),4 字节随机数 |
| Payload data | 可变 | 实际传输的数据(文本 / 二进制),若 MASK=1 需先解掩码 |
通俗解读帧格式(以客户端发 "你好" 为例)
- FIN=1:这是完整的 "你好" 消息,没有分片;
- Opcode=0x1:这是文本帧,数据是 UTF-8 格式;
- MASK=1:客户端发送的帧必须掩码,后续带 4 字节掩码密钥;
- Payload length=2:"你好" 的 UTF-8 编码是 2 个字节;
- Masking-key :比如随机生成
0x12 0x34 0x56 0x78; - Payload data :"你好" 的 UTF-8 二进制数据(
0xE4 0xBD 0xA0 0xE5 0xA5 0xBD?注:实际 "你好" 是 6 字节,此处仅举例),先通过掩码密钥处理后再传输。
关键规则
- 客户端→服务器的所有数据帧,MASK 必须为 1(强制掩码);
- 服务器→客户端的所有数据帧,MASK 必须为 0(禁止掩码);
- 若违反该规则,对方会直接断开连接。
二、核心:WebSocket 掩码机制(怎么加 / 解掩码?)
掩码的本质是客户端用 4 字节随机密钥,对负载数据做简单的异或(XOR)运算,目的是 "打乱" 数据二进制,防御代理污染攻击。
1. 掩码运算规则(固定算法)
假设:
- 掩码密钥(Masking-key):4 字节数组
mask[0], mask[1], mask[2], mask[3]; - 负载数据(Payload data):字节数组
data[0], data[1], ..., data[n-1]; - 掩码后的数据:
encrypted_data[i] = data[i] XOR mask[i mod 4]; - 解掩码:
decrypted_data[i] = encrypted_data[i] XOR mask[i mod 4](和加密是同一个运算)。
2. 示例:简单掩码 / 解掩码计算
比如:
- 掩码密钥:
[0x12, 0x34, 0x56, 0x78]; - 原始数据(1 字节):
0xE4; - 掩码计算:
0xE4 XOR 0x12 = 0xF6(i=0,i mod 4=0,用 mask [0]); - 解掩码:
0xF6 XOR 0x12 = 0xE4(还原原始数据)。
3. 为什么只有客户端帧需要掩码?
因为代理污染攻击的风险只存在于 "客户端→服务器" 的方向(后文详解),服务器→客户端的帧无需掩码,可减少运算开销。
三、防护目标:代理污染攻击(为什么需要掩码?)
1. 先理解:什么是 "HTTP 代理 / 缓存污染"?
早期互联网中,大量中间节点(如 HTTP 代理、缓存服务器、反向代理)是为 HTTP 协议设计的 ------ 这些节点会 "无脑" 解析二进制数据,试图识别 HTTP 报文的特征(比如 \r\n、Host:、GET 等),并缓存 / 转发数据。
如果 WebSocket 客户端直接发送未掩码的明文二进制数据,可能会出现:
- 数据中恰好包含 HTTP 报文的特征字符串(比如
GET / HTTP/1.1\r\n); - 中间代理服务器误将 WebSocket 帧识别为 "HTTP 请求";
- 代理服务器缓存这个 "伪 HTTP 请求",并将其返回给后续访问该代理的用户 ------ 这就是代理污染攻击(也叫 "缓存污染")。
2. 代理污染攻击的具体场景(无掩码时的风险)
假设客户端通过 WebSocket 发送一条文本消息:"GET /admin HTTP/1.1\r\nHost: hack.com\r\n"(只是普通聊天内容,无攻击意图):
- 若没有掩码,这条消息的二进制数据会原封不动通过代理;
- 代理服务器识别到
GET /admin HTTP/1.1这个 HTTP 特征,误以为这是一个 HTTP 请求; - 代理服务器会缓存 "
/admin路径对应hack.com的响应"; - 后续其他用户通过该代理访问
http://正常网站/admin时,代理会返回缓存的hack.com内容 ------ 导致正常网站的缓存被污染,用户访问到错误数据。
3. 掩码如何防御代理污染攻击?
掩码通过 "随机异或打乱数据二进制",确保:
- 客户端发送的 WebSocket 帧数据中,几乎不可能出现连续的 HTTP 特征字符串 (比如
GET、\r\n、Host:); - 中间代理服务器无法识别出 HTTP 报文特征,也就不会误解析、误缓存;
- 即使数据中原本包含 HTTP 特征,掩码后也会被打乱,彻底避免代理污染。
关键补充:掩码不是加密
- 掩码的密钥是 4 字节随机数,且和数据一起传输(Masking-key 字段),任何人拿到帧数据和密钥都能解掩码;
- 掩码的唯一目的是 "打乱数据特征",而非 "保护数据安全"(数据安全需靠 WSS 加密);
- 服务器收到掩码帧后,会先用 Masking-key 解掩码,再处理数据 ------ 这个过程对开发者透明(
ws库、浏览器会自动处理)。
四、代码层面的体现(掩码是透明的)
你之前的示例代码中,没有手动处理掩码,因为:
- 客户端(浏览器) :调用
ws.send("你好")时,浏览器会自动:- 生成 4 字节随机掩码密钥;
- 对 "你好" 的 UTF-8 数据做掩码运算;
- 按帧格式封装数据,设置 MASK=1,携带 Masking-key;
- 发送帧到服务器。
- 服务端(ws 库) :收到帧后,会自动用 Masking-key 解掩码,你在
ws.on('message')中拿到的是还原后的明文 "你好"------ 无需手动处理。
验证:强制关闭掩码会怎样?
如果通过底层编程(如手动构造帧)发送 MASK=0 的客户端帧,服务器会直接断开连接,并抛出类似 "Received unmasked frame from client" 的错误 ------ 这是协议强制要求。
五、核心总结
- WebSocket 消息格式:所有消息以 "帧" 为单位传输,帧包含类型、长度、掩码标识、密钥、数据等字段,客户端帧必须掩码,服务器帧禁止掩码;
- 掩码机制:客户端用 4 字节随机密钥对数据做异或运算,服务器收到后反向还原,过程对开发者透明;
- 掩码的唯一目的:打乱数据二进制特征,防止中间 HTTP 代理 / 缓存服务器误识别为 HTTP 报文,从而避免代理污染攻击;
- 关键区别:掩码≠加密(无安全保护),WSS(WebSocket over TLS)才是加密传输,掩码仅解决协议兼容问题。