Injection CRLF : détourner les en-têtes HTTP et forger des logs
Une injection CRLF exploite la signification structurelle de deux caractères de contrôle : le retour chariot CR (%0D, \r) et le saut de ligne LF (%0A, \n). Leur séquence \r\n marque la fin d’une ligne d’en-tête HTTP, et une double séquence sépare les en-têtes du corps. Les mêmes caractères délimitent aussi les entrées d’un fichier de log. Quand une entrée utilisateur contenant ces caractères est reflétée dans un en-tête ou un journal sans filtrage, l’attaquant contrôle la structure du message.
Principe
Section intitulée « Principe »Une application insère une valeur cliente dans un en-tête de réponse ou une ligne de log :
Set-Cookie: session=<valeur>2025-01-15 10:42 LOGIN status=failed user=<username>Si <valeur> ou <username> peut contenir un %0D%0A, la valeur ne reste plus sur une seule ligne. Tout ce qui suit la séquence est interprété comme une nouvelle ligne d’en-tête ou une nouvelle entrée de journal.
Injection d’en-tête HTTP
Section intitulée « Injection d’en-tête HTTP »En glissant un CRLF suivi d’un en-tête arbitraire dans un paramètre reflété, on ajoute un en-tête à la réponse :
?username=bob%0D%0AInjected-Header:+crlf-okLa réponse construite par le serveur devient :
HTTP/1.1 200 OK...X-Username: bobInjected-Header: crlf-okSelon le contexte, ce levier permet de poser un Set-Cookie choisi par l’attaquant, de manipuler la mise en cache, ou, avec une double séquence %0D%0A%0D%0A, d’écrire dans le corps de la réponse. Cette dernière variante, le response splitting, sert de tremplin à des injections plus larges quand un intermédiaire de cache est présent.
Injection et forge de logs
Section intitulée « Injection et forge de logs »Le même mécanisme s’applique aux journaux applicatifs. Une application qui enregistre les tentatives de connexion sous la forme :
2025-01-15 10:42 LOGIN status=failed user=<username>est vulnérable si <username> accepte un CRLF. En injectant une séquence suivie d’une ligne fabriquée, l’attaquant ajoute une entrée entièrement fausse :
?username=bob%0D%0A2025-01-15+10:43+LOGIN+status=success+user=bobLe journal contient alors deux lignes, dont une forgée qui fait croire à une authentification réussie :
2025-01-15 10:42 LOGIN status=failed user=bob2025-01-15 10:43 LOGIN status=success user=bobEn jouant sur plusieurs sauts de ligne, on scinde une valeur en plusieurs entrées pour brouiller la lecture ou masquer une trace :
?username=bob+authenticated.%0D%0Aguest%0D%0ACette falsification de journaux compromet la valeur probante des logs : une investigation qui s’y fie peut être orientée vers de fausses conclusions.
Détecter la vulnérabilité
Section intitulée « Détecter la vulnérabilité »Tester un paramètre reflété avec une séquence CRLF et un marqueur reconnaissable :
# En-tête injecté observable dans la réponsecurl -si "http://cible/?username=test%0D%0AX-Injected:+crlf-ok" | grep -i "X-Injected"Si l’en-tête X-Injected: crlf-ok apparaît dans la réponse, le serveur ne filtre pas les CRLF. Pour les logs, l’effet n’est visible que si l’on a accès au journal ou à une interface qui le restitue.
Remédiation
Section intitulée « Remédiation »Rejeter ou neutraliser CR et LF
Section intitulée « Rejeter ou neutraliser CR et LF »Toute donnée destinée à un en-tête ou à un log doit être débarrassée des caractères \r et \n, ou rejetée si elle en contient.
import re
def sanitize_header_value(value: str) -> str: if "\r" in value or "\n" in value: raise ValueError("Caractère de contrôle interdit") return value
# Pour un log, neutraliser les sauts de ligne du contenu utilisateursafe = re.sub(r"[\r\n]", " ", username)logger.info("LOGIN status=failed user=%s", safe)S’appuyer sur les API du framework
Section intitulée « S’appuyer sur les API du framework »Les serveurs et bibliothèques récents rejettent d’eux-mêmes les CRLF dans les valeurs d’en-tête. Utiliser les API dédiées (response.set_cookie, response.headers[...]) plutôt que de concaténer manuellement une chaîne d’en-tête laisse cette protection s’appliquer.
Encoder les logs de façon structurée
Section intitulée « Encoder les logs de façon structurée »Un journal au format structuré (JSON par ligne) où chaque champ est encodé rend l’injection de fausses entrées inopérante : un saut de ligne dans une valeur est échappé et reste dans le champ, sans créer de nouvelle entrée.
À retenir
Section intitulée « À retenir »Une injection CRLF ne repose que sur deux caractères, mais ils portent la structure même des messages HTTP et des journaux. Reflétés sans filtrage dans un en-tête, ils ajoutent des en-têtes ou scindent la réponse ; dans un log, ils fabriquent de fausses entrées et ruinent la fiabilité des traces. La parade est constante : refuser \r et \n dans toute donnée utilisateur insérée dans un en-tête ou un journal, et passer par les API du framework plutôt que par de la concaténation.