Repository navigation
Description
两条 RFC 6455 的 MUST 没有实现:未掩码的客户端帧、非法 UTF-8 的文本帧
在 master(1.1.7,本地从源码构建)上打 WebSocket 协议一致性时发现的。解析器只对这两条不做校验,其余都做了 —— 所以这不是"WS 实现不完整",而是两个具体的检查漏了。
1. 未掩码的客户端帧被当成正常消息
发送:一个掩码位为 0 的合法 TEXT 帧(内容 "unmasked")
实测:服务端把它原样回显了(收到 data b'unmasked')
期望:以 1002 关闭连接
RFC 6455 §5.1:
The server MUST close the connection upon receiving a frame that is not masked.
... a server MUST use the 1002 status code.
掩码存在的理由就是防中间代理缓存投毒(§10.3),所以服务端不强制等于把这条防护让掉。
2. 非法 UTF-8 的文本帧被原样回显
发送:TEXT 帧,负载是 \xff\xfe\xfd(不是合法 UTF-8)
实测:服务端原样回显(收到 data b'\xff\xfe\xfd')
期望:以 1007 关闭
RFC 6455 §8.1:
When an endpoint is to interpret a byte stream as UTF-8 but finds that the byte stream is not, in fact, a valid UTF-8 stream, that endpoint MUST Fail the WebSocket Connection.
对照组:其余校验都是对的
| 检查 | 服务端行为 |
|---|---|
| RSV 位被置(未协商扩展) | ✅ close 1002 |
| 保留 opcode(0x3) | ✅ close 1002 |
| 分片消息(首片 + 续片 + 末片) | ✅ 正确重组后回显 |
| 100 KB 消息(64 位长度编码) | ✅ 完整回显 |
| Close 帧 | ✅ 以 1000 应答 |
所以校验框架是在的,只是这两条没有。
代码位置与建议改法
掩码:src/web/uvcpp_ws_connection.cpp 里控制帧/数据帧是在一处分支里分开处理的(switch (frame.opcode) 那一支是控制帧,之后是数据帧)。掩码检查要在这个分叉之前做 —— 它对所有帧类型都适用(控制帧也必须掩码,§5.5):
// 服务端:客户端发来的帧必须掩码(RFC 6455 §5.1)。
// 注意方向:客户端一侧反过来 —— 服务端发来的帧必须**不**掩码,同样是 1002。
if (is_server_side() && !frame.masked) {
protocol_error(ws_close_code::PROTOCOL_ERROR, "client frame must be masked");
return;
}(uvcpp_ws_parser 现在是"有掩码就解、没有也不拒" —— parse_frame_header 里 if (frame_.masked) { state_ = MASK_KEY; ... },缺一个方向性的要求。因为解析器两边共用,这个判断放在连接层更合适,需要知道自己是服务端还是客户端。)
UTF-8:deliver_message() 里,解压之后、调 on_text_ 之前,正好是校验点 —— 校验的必须是应用将要看到的那份字节(压缩时必须验解压后的):
if (op == ws_opcode::TEXT) {
if (!is_valid_utf8(data, len)) {
message_payload_.clear();
protocol_error(ws_close_code::INVALID_PAYLOAD, "text frame is not valid UTF-8");
return;
}
...
}INVALID_PAYLOAD(1007)这个关闭码文件里已经在用了(permessage-deflate 解压失败那一支),所以不用新增任何基础设施。
(严格说 §8.1 还要求分片文本边收边校验,好在坏字节到达时就失败;但"消息收完再验"已经满足"MUST Fail the Connection"这条主要要求,实现成本也低得多。可以先把这条补上。)
复现
两条都不需要特殊工具,一个裸 socket 就能发:
# 未掩码的 TEXT 帧(fin=1, opcode=1, mask=0, len=8)
sock.sendall(bytes([0x81, 0x08]) + b"unmasked")
# 非法 UTF-8 的 TEXT 帧(fin=1, opcode=1, mask=0, len=3)
sock.sendall(bytes([0x81, 0x03]) + b"\xff\xfe\xfd")完整探查脚本(7 条,含上面那组对照)我这边有,需要的话可以贴上来或者整理成一个 tests/functional/ 下的用例。
影响评估
两条都是一致性缺陷,不是内存/崩溃类问题:
- 未掩码:服务端本身不会因此出错,但它放掉的是"客户端必须掩码"这条约束 —— 在中间代理存在时,这正是掩码要防的场景。
- 非法 UTF-8:服务端只是转发字节,自己不受影响;但一个把文本原样转给浏览器的应用(聊天室这类)会把非法字节传下去,客户端那边才炸。
两条都不难改,改动集中在 uvcpp_ws_connection.cpp,不需要动解析器。
Activity
sercebr commentedon Sep 19, 2026
修复在 #9。
补这两条检查时还暴露出第三条同源缺陷:框架自己的客户端也从不掩码(
send_data()造帧时不设f.masked),所以两侧发的都是未掩码帧 —— 这在服务端没人检查时一直看不出来。一加上服务端检查,test_web_ws_func的 echo 与test_web_ws_client_api_func当场失败,因为框架客户端发不出一帧自己的服务端愿意收的数据。所以 #9 把客户端掩码也一并补了(§5.1 的另一半,同样从没实现过)。验证:仓库自带的 7 个 WS 用例全部通过,另有独立探针 7/7(协议一致性)与 19/19(UTF-8 边界)。
Antruly commentedon Sep 20, 2026
修复随 #9 合并落地(
87df278+ef5207e+fa2b97d,1.1.25)。两条 RFC 6455 MUST(§5.1 客户端帧必须掩码、§8.1 文本帧负载必须合法 UTF-8)以及你顺带挖出来的第三条同源缺陷(框架客户端从不掩码)现在都是真的实现了。
合并时我补了一条你没写的用例:未掩码的控制帧。它和数据帧那条钉的是两个不同位置 —— 一条钉"检查在不在",一条钉"检查放对了没有"。变异证明它非空转:把检查改成放走控制帧(
&& !frame.is_control_frame()),数据帧那条照旧 PASS、只有它 FAIL。本机:0 个
error C/error LNK,77/77 ctest,页面堆门禁 77 个用例裸跑 + 完整页堆各一遍通过。