
Security Advisory: HTTP Header Injection via Unvalidated CR and LF in Header Values (tiny_http)
Identifiant CVE attribué : CVE-2026-66753
tiny_http ne rejette pas les retours chariot ni les sauts de ligne dans les valeurs d'en-tête HTTP, dans les deux sens.
Côté requête, read_next_line ne termine une ligne d'en-tête que sur CRLF, de sorte qu'un LF isolé survit dans la valeur analysée et parvient à l'application. Côté réponse, Header::from_bytes ne valide que le fait que les octets sont en ASCII, et le rédacteur de réponse émet les valeurs telles quelles, donc une valeur contenant CRLF scinde la réponse.
Les applications qui reflètent une valeur d'en-tête de requête dans un en-tête de réponse, ou qui re-sérialisent les en-têtes de requête sur une autre connexion, héritent donc d'une primitive d'injection sans aucune indication que quelque chose ne va pas.
Repo URL : https://github.com/tiny-http/tiny-http
| Affectées | toutes les versions publiées jusqu'à 0.12.0 incluse (2022-10-06), la version actuelle |
| Vérifiées | 0.6.2, 0.6.3, 0.8.0, 0.9.0, 0.10.0, 0.11.0, 0.12.0 |
| Corrigée dans | aucune version corrigée au moment de la rédaction |
Ceci est distinct de RUSTSEC-2020-0031 / CVE-2020-35884, qui concernait l'analyse de Transfer-Encoding et a été corrigé dans 0.6.3 et 0.8.0. Le comportement décrit ici est présent à la fois avant et après ce correctif.
CWE-113 (neutralisation inappropriée des séquences CRLF dans les en-têtes HTTP), un cas particulier de CWE-93 (injection CRLF).
Score de base CVSS 4.0 : 6,3 (Moyen)
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 distant, non authentifié, envoyant du HTTP brut à n'importe quel serveur tiny_http. Aucun identifiant ni interaction utilisateur.
Deux formes d'utilisation transforment cela en vulnérabilité :
Location construits à partir de données de requête et la gestion des cookies. Un LF isolé dans la valeur de requête atteint la réponse, et si l'application construit elle-même la valeur de réponse, elle peut porter un CRLF complet.net/http et Python http.server ont tous deux été mesurés comme l'acceptant.tiny_http-0.12.0/src/client.rs, lignes 80 à 102. La boucle ne retourne que lorsqu'un LF est précédé d'un CR. Tout autre octet, y compris un LF isolé ou un CR isolé, est poussé dans le tampon de ligne à la ligne 100 :
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 est 0x0A et CR est 0x0D, tous deux en ASCII valide, donc AsciiString::from_ascii à la ligne 94 les accepte.
tiny_http-0.12.0/src/common.rs, lignes 184 à 191, ne fait ensuite que découper (trim) la valeur. trim supprime les espaces de début et de fin mais laisse les octets intérieurs intacts :
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(())?;
Une paire CRLF ne peut pas survivre, car elle termine la ligne, mais un LF isolé, un CR isolé et des séquences telles que \n\r le peuvent.
tiny_http-0.12.0/src/common.rs, lignes 166 à 172, ne vérifie que l'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, lignes 99 à 104, écrit la valeur sans échappement :
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 }
Notez que HeaderField::from_str à la ligne 226 rejette les espaces dans un nom de champ, mais HeaderField::from_bytes à la ligne 171 ne le fait pas, donc les noms d'en-têtes de réponse ne sont pas vérifiés non plus.
Étape 1. Démarrez un serveur qui affiche une valeur d'en-tête de requête analysée et renvoie une valeur contenant CRLF dans un en-tête de réponse.
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);
}
}
Étape 2. Envoyez une requête dont la valeur X-Test contient un LF nu. Dans la commande ci-dessous, \n est un saut de ligne unique et \r\n est un CRLF. La distinction est l'objet du test, ne laissez donc pas un éditeur la normaliser.
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
Résultat sur stdout. Le LF est toujours à l'intérieur de la valeur analysée :
parsed X-Test value = "aaa\nbbb"
Résultat sur le fil. L'en-tête de réponse s'est scindé en deux :
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
En soi, tiny_http ne réémet pas les en-têtes de requête, donc le comportement côté requête est une primitive latente plutôt qu'une compromission directe. Il devient exploitable dans tout consommateur qui transmet ou reflète des valeurs d'en-tête, où il produit un détournement de requête (request smuggling) contre des backends tolérants aux LF ou une injection d'en-têtes dans la réponse.
Le comportement côté réponse est directement exploitable dans toute application qui place du texte influencé par l'attaquant dans une valeur d'en-tête : injection de script dans le contexte de l'origine, empoisonnement du cache, fixation de session via un Set-Cookie injecté, et remplacement des en-têtes de sécurité.
À titre de comparaison, la crate http rejette CR et LF dans HeaderValue::from_str, c'est pourquoi les piles logicielles construites dessus n'exposent aucun de ces comportements.
Rejetez les caractères de contrôle aux deux limites.
Dans read_next_line (src/client.rs ligne 80), traitez un CR nu ou un LF nu dans une ligne d'en-tête comme une erreur de protocole et renvoyez 400, plutôt que de l'incorporer dans la valeur. Vous pouvez également les rejeter dans Header::from_str (src/common.rs ligne 184) après la division.
Dans Header::from_bytes (src/common.rs ligne 166), rejetez les valeurs contenant 0x0D, 0x0A ou 0x00, et rejetez les noms de champ contenant autre chose que l'ensemble de jetons de la RFC 9110. Le retour de Err y est déjà géré par les appelants, car la signature est faillible.