HTTP/1.1 在安全层面已经“走到尽头”
内容基于 PortSwigger 官方研究《HTTP/1.1 Must Die: The Desync Endgame》
https://portswigger.net/research/http1-must-die
HTTP/1.1 是一个基于文本的协议,且请求边界没有明确分隔符,仅依赖 Content-Length、Transfer-Encoding 等头部来判断长度。协议中没有强制的消息帧边界。
现代web的消息路径一般是:
1 | 浏览器 → CDN → WAF → 反向代理 → 应用服务器 |
如果不同组件的解析规则不一致,Desync攻击就变得非常容易。
Desync 攻击是如何构造的?
下面以经典CL.TE为例:
在 HTTP/1.1 中,有两个主要的头部用来告知服务器请求体的长度:
- Content-Length (CL): 直接以字节数表示请求体的长度。
- Transfer-Encoding: chunked (TE): 告知服务器数据是分块传输的,每块前面有一个数字,表示数据长度,最后以一个 0 长度的块结束。
当前端服务器和后端应用服务器对 HTTP 请求定界(判断请求在哪里结束)的逻辑不一致时,攻击者可以“走私”一部分请求内容到下一个正常的请求中。
构造恶意请求: 攻击者发送一个同时包含 Content-Length 和 Transfer-Encoding: chunked 的请求。
1 | POST / HTTP/1.1 |
- 前端(CL)视角: 前端组件看到 Content-Length:15 。它数了数,发现从 0 到 SMUGGLED\r\n 刚好是 15 个字节(含换行)。于是它把这全部内容转发给后端。
- 后端(TE)视角: 后端组件看到 Transfer-Encoding: chunked。它开始读分块,读到第一个分块长度是 0,它就认为:“OK,这个请求到此结束了。”
注意:这里的前端/后端并不是指浏览器/服务器,而是指“浏览器 → CDN → WAF → 反向代理 → 应用服务器”这一路径上的前后两个组件。
- 结果: 后端处理完第一个请求并返回。但此时,字符串 SMUGGLED 仍然留在服务器的连接缓冲区里。
- 后果: 当下一个正常用户发送 GET /index.html 时,后端实际接收到的是:
1 | SMUGGLED |
这通常会导致 404 错误、权限绕过,精心构造的SMUGGLED字符串甚至可以劫持用户的会话。

HTTP1.1中,为什么Desync问题不易修复
我们还是以CL.TE为例,厂商在A打补丁:
1 | “如果同时存在 CL 和 TE,统一按 CL 解析” |
厂商B打补丁:
1 | “如果同时存在 CL 和 TE,统一按 TE 解析” |
各厂商的修复策略不一样,可能产生新的desync。然后又打上新的补丁。
1 | if (hasCL && hasTE && isProxy) { ... } |
而 CDN、WAF、代理、负载均衡、服务器。 全部在独立演化这些规则。
攻击者只需要找到一条路径,即可实现攻击。
HTTP/2如何修复此问题
以上问题的出现,问题不在某条代码,而在协议模型本身:
| HTTP/1.1 特性 | 安全后果 |
|---|---|
| 没有帧边界 | 必须靠推断请求结束位置 |
| 文本协议 | 解析规则复杂、易产生歧义 |
| 向后兼容包袱 | 无法强制统一解析行为 |
| 多组件链路 | 任意两层差异即可被利用 |
只要还在用 HTTP/1.1, 就必然存在某个角落的解析差异。
HTTP/2 把请求彻底改成了二进制帧协议,每一帧的结构是固定的。
1 | +-----------------------------------------------+ |
核心安全点
- Length 字段明确规定帧长度
- 解析器按 Length 精确切帧
- 不需要 Content-Length / TE
- 多路复用在帧层完成
解析过程是数学确定的,没有猜测空间。