
Security Advisory: HTTP-Request-Smuggling über nicht geparste Transfer-Encoding-Werte (tiny_http)
Zugewiesene CVE-ID: CVE-2026-66752
tiny_http prüft lediglich, ob ein Transfer-Encoding-Header vorhanden ist. Der Wert selbst wird nie betrachtet. Jeder Wert – einschließlich Kodierungen, die nicht chunked sind, sowie Kodierungslisten, deren letztes Element nicht chunked ist – führt dazu, dass der Body als Chunk dekodiert wird, und Content-Length wird gleichzeitig verworfen.
Ein Frontend, das die Transfer-Kodierung korrekt parst, rahmt eine solche Anfrage anders ein als tiny_http. Zwei Teilnehmer mit zwei unterschiedlichen Rahmungen auf einer Verbindung sind die Grundvoraussetzung für Request-Smuggling. Das Senden eines nicht-chunked Bodys mit einer nicht-chunked Kodierung führt außerdem dazu, dass tiny_http das Lesen des Bodys abbricht und überhaupt nicht antwortet.
Repo-URL: https://github.com/tiny-http/tiny-http
| Betroffen | alle veröffentlichten Versionen bis einschließlich der aktuellen Version 0.12.0 (2022-10-06) |
| Verifiziert | 0.6.2, 0.6.3, 0.8.0, 0.9.0, 0.10.0, 0.11.0, 0.12.0 |
| Behoben in | zum Zeitpunkt des Schreibens keine behobene Version |
CWE-444 (Inkonsistente Interpretation von HTTP-Anfragen).
CVSS-4.0-Basisscore 6.3 (Mittel)
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
Ein entfernter, nicht authentifizierter Client, der rohes HTTP sendet. Keine Anmeldedaten oder Benutzerinteraktion erforderlich.
Der Smuggling-Fall setzt voraus, dass tiny_http hinter einem Frontend oder CDN sitzt, das Transfer-Encoding und Content-Length weiterleitet und RFC 9112 Abschnitt 6.1 korrekt anwendet, d. h. es behandelt eine Kodierungsliste, deren letztes Element nicht chunked ist, als nicht chunked und fällt auf Content-Length zurück. Dieses Frontend und tiny_http sind sich dann uneinig darüber, wo der Body endet.
Der Fall der unbeantworteten Anfrage benötigt nichts weiter als die Möglichkeit, eine Verbindung herzustellen.
tiny_http-0.12.0/src/request.rs, Zeilen 143 bis 153. Der Header wird gefunden und geklont, aber der Wert wird lediglich auf sein Vorhandensein geprüft:
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, Zeilen 218 bis 221, wendet anschließend den Chunked-Decoder bedingungslos an:
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 ist eine Option<AsciiString>, die den rohen Wert enthält, dessen Inhalt jedoch nie untersucht wird. RFC 9112 Abschnitt 6.1 verlangt, dass die letzte Kodierung einer Anfrage chunked ist, und dass ein Server die Nachricht andernfalls ablehnt; Abschnitt 6.3 verlangt, eine Anfrage abzulehnen, die sowohl Transfer-Encoding als auch Content-Length enthält.
Schritt 1. Starten Sie einen Server, der die Framing-Header und den gelesenen Body ausgibt.
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"));
}
}
Schritt 2. Senden Sie eine Anfrage mit Transfer-Encoding: identity, einem passenden Content-Length und einem einfachen Body. Jedes Frontend würde diesen Body als die fünf Bytes hello einrahmen.
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
Ergebnis. tiny_http dekodierte einen Body, der nicht chunked ist, als Chunk, verwarf ihn und sendete keine Antwort. Der Client wartet, bis sein eigenes Timeout erreicht ist, und die Verbindung wird verbraucht.
TE=Some("identity") CL=Some("5") read=Err(Custom { kind: InvalidInput, error: DecoderError }) body=""
Schritt 3. Senden Sie eine Kodierungsliste, deren letztes Element nicht chunked ist.
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
Ergebnis. tiny_http akzeptiert sie und dekodiert als chunked, obwohl RFC 9112 Abschnitt 6.1 die Ablehnung verlangt:
TE=Some("chunked, identity") CL=None read=Ok(5) body="hello"
Zum Vergleich: Content-Length allein und ein korrektes Transfer-Encoding: chunked verhalten sich beide ordnungsgemäß und ergeben body="hello".
Wenn tiny_http hinter einem Frontend sitzt, das Transfer-Kodierungen korrekt parst, sind sich die beiden Komponenten uneinig darüber, wo der Request-Body endet. Der Angreifer wählt die Bytes auf beiden Seiten dieser Uneinigkeit – das ist das Standard-Setup, um eine Anfrage an der Routing- und Zugriffskontrolle des Frontends vorbeizuschmuggeln.
Unabhängig von einem Frontend zeigen die Schritte 2 und 3 eine trivial fehlerhafte Anfrage, die einen Worker blockiert und überhaupt keine Antwort erhält, sodass der Client nicht erkennen kann, dass die Anfrage abgelehnt wurde.
Parsen Sie Transfer-Encoding als die kommagetrennte Liste, die es ist, in src/request.rs um Zeile 144:
Verlangen Sie, dass die letzte Kodierung einer Anfrage mit Body chunked ist, und lehnen Sie jede andere Kodierung mit 400 ab, anstatt sie als Chunk zu dekodieren. Lehnen Sie eine Anfrage ab, die sowohl Transfer-Encoding als auch Content-Length enthält, wie es RFC 9112 Abschnitt 6.3 für einen Server verlangt, der kein Proxy ist, anstatt Content-Length in Zeile 150 stillschweigend zu ignorieren.
Wenn das Lesen des Bodys fehlschlägt, senden Sie eine 400-Antwort, anstatt die Anfrage ohne Antwort zu verwerfen.