Aller au contenu

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.

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.

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-ok

La réponse construite par le serveur devient :

HTTP/1.1 200 OK
...
X-Username: bob
Injected-Header: crlf-ok

Selon 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.

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=bob

Le 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=bob
2025-01-15 10:43 LOGIN status=success user=bob

En 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%0A

Cette falsification de journaux compromet la valeur probante des logs : une investigation qui s’y fie peut être orientée vers de fausses conclusions.

Tester un paramètre reflété avec une séquence CRLF et un marqueur reconnaissable :

Fenêtre de terminal
# En-tête injecté observable dans la réponse
curl -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.

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 utilisateur
safe = re.sub(r"[\r\n]", " ", username)
logger.info("LOGIN status=failed user=%s", safe)

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.

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.

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.