Une injection de requête HTTP peut-elle commencer par un saut de ligne ?
L'injection de requête HTTP peut commencer par un saut de ligne caché. Découvrez comment rejeter CRLF dans les destinations et les en-têtes avant de rendre des requêtes authentifiées.

Un injecteur d'identifiants peut garder une clé API hors du contexte d'un agent tout en envoyant cette clé dans la mauvaise requête. Le problème survient lorsque la passerelle traite l'entrée de l'agent comme du texte inoffensif, puis l'insère dans la syntaxe HTTP une fois la couche d'identifiants terminée.
J'ai vu des équipes investir beaucoup d'efforts dans le stockage en coffre-fort, les invites d'approbation et les journaux d'audit, puis laisser un formateur de chaînes entre l'agent et le réseau. Ce formateur devient une partie de la frontière de sécurité. S'il accepte un retour chariot ou un saut de ligne dans une destination, un nom d'en-tête personnalisé ou une valeur d'en-tête fournie par l'agent, il peut permettre à des données de créer la structure d'une requête.
La correction est volontairement banale : rejetez les retours chariot et les sauts de ligne avant le rendu de la requête, analysez les entrées structurées au lieu de les composer avec des chaînes, et testez les octets qui quittent le processus. N'essayez pas de corriger cela en supprimant ou en remplaçant des caractères. Une requête rejetée est honnête. Une requête réparée peut être une requête différente.
Une injection de requête HTTP peut-elle commencer par un saut de ligne ?
Oui. En HTTP/1.1, un saut de ligne sépare les parties du message. Si le code place du texte non fiable dans une ligne de requête ou d'en-tête sans faire respecter cette limite, un attaquant peut utiliser CRLF, un retour chariot suivi d'un saut de ligne, pour terminer la ligne en cours et en commencer une autre.
Un moteur de rendu vulnérable simplifié semble souvent inoffensif :
GET {path} HTTP/1.1\r\n
Host: {host}\r\n
Authorization: Bearer {vault_token}\r\n
{custom_name}: {agent_value}\r\n
\r\n
Supposons que l'agent fournisse cette valeur :
blue\r\nX-Forwarded-Host: internal.example
Les octets rendus contiennent maintenant un en-tête supplémentaire. L'identifiant n'a pas été divulgué dans l'invite de l'agent. Pourtant, l'agent a influencé la requête authentifiée d'une manière que le concepteur n'avait pas prévue.
Le même type de bug apparaît dans une chaîne de destination lorsqu'une passerelle construit elle-même la première ligne, ou lorsqu'une couche analyse une URL et qu'une autre combine ensuite un chemin, un hôte ou une cible de proxy avec du formatage de texte. Le résultat exact dépend de la bibliothèque HTTP et du comportement des intermédiaires. Cette incertitude n'est pas une défense. Lorsque des composants ne s'accordent pas sur les limites d'un message, une petite omission de validation peut devenir un incident de sécurité.
RFC 9110 définit les lignes de champ HTTP comme une syntaxe composée d'un nom de champ, de deux-points et d'une valeur de champ. Il considère aussi les sauts de ligne comme des délimiteurs de message, et non comme du contenu ordinaire dans un champ. Cette distinction compte davantage qu'un avertissement général demandant d'« échapper l'entrée utilisateur ». Il n'existe pas de forme échappée utile pour un saut de ligne dans un champ d'en-tête dans ce cas. Rejetez-le.
Un coffre-fort protège les identifiants, pas la forme de la requête
L'injection d'identifiants et l'injection de requête répondent à deux problèmes distincts. La première garde le secret hors de portée de l'agent. La seconde garantit que la requête qui reçoit le secret a la forme approuvée par un humain.
Prenons un outil qui permet à un agent d'appeler une API de facturation avec un jeton bearer détenu dans un coffre-fort et accepte un en-tête personnalisé facultatif. Un développeur peut juger l'en-tête peu risqué parce que le jeton n'apparaît jamais dans les arguments de l'outil. Ce raisonnement oublie la proximité. Une ligne injectée peut ajouter un en-tête de transfert, modifier une longueur de contenu, dupliquer un en-tête applicatif ou empoisonner une étiquette d'audit utilisée plus loin. L'identifiant reste secret alors que l'action devient dangereuse.
Les destinations méritent la même méfiance. Un nom d'hôte qui ressemble à une donnée peut devenir de la syntaxe d'autorité. Un chemin peut devenir une cible de requête. Un sélecteur de proxy peut changer l'endroit où le client se connecte. Si l'agent peut influencer l'une de ces valeurs, la passerelle doit décider quelles formes sont valides avant d'y joindre un identifiant.
C'est pourquoi une liste blanche de noms d'hôte approuvés est utile, mais insuffisante. Elle limite les destinations possibles d'une URL analysée. Elle ne prouve pas que chaque composant a reçu la même URL analysée et n'empêche pas les sauts de ligne dans les informations utilisateur, le remplacement d'un en-tête ou une chaîne ajoutée après le contrôle de la liste blanche. La frontière doit rejeter les caractères de contrôle et conserver des valeurs structurées jusqu'au client HTTP.
Rejeter CR et LF avant même de créer un objet de requête
La règle la plus sûre tient en une phrase : les champs de destination contrôlés par l'agent, les noms d'en-tête personnalisés et leurs valeurs ne doivent contenir ni \r ni \n après décodage.
Appliquez cette règle avant de construire une URL, de créer des en-têtes, de choisir une option cliente ou d'écrire une entrée de journal qui alimentera plus tard une nouvelle tentative. Vérifiez la chaîne d'origine afin de détecter les caractères de contrôle littéraux. Décodez une seule fois selon le contrat d'entrée, puis vérifiez de nouveau la chaîne décodée. Ne décodez pas à répétition jusqu'à ce que plus rien ne change. Le décodage répété transforme un contrat clair en conjecture et peut produire des résultats surprenants.
Une fonction de validation compacte a la bonne forme lorsqu'elle refuse de réparer l'entrée :
validateRequestText(field, value):
if value contains "\r" or "\n":
fail(field + " contains a line break")
return value
Dans du code réel, validez la valeur de chaîne effective du langage, pas sa représentation imprimée. Une charge JSON peut contenir "\n" ; après analyse du JSON, cela devient un caractère de saut de ligne. Chercher les deux caractères visibles barre oblique inverse et n ne détecte pas la valeur dangereuse.
N'appelez pas trim() avant de continuer. Dans la plupart des implémentations, cette fonction retire uniquement les extrémités, donc un saut de ligne au milieu subsiste. Remplacer les sauts de ligne par des espaces est pire d'une autre façon : cela change l'action demandée tout en ne laissant aucun enregistrement clair indiquant que l'agent a demandé une syntaxe interdite. Traitez l'entrée comme invalide et arrêtez-vous avant toute E/S réseau.
La réponse de rejet doit identifier le champ sans refléter de contenu sensible. custom header value contains a line break donne suffisamment d'informations à l'appelant. Répéter la valeur entière peut placer des identifiants ou des données privées dans une transcription de terminal.
Les noms d'en-tête exigent un contrat plus strict que les valeurs
Une valeur d'en-tête peut légitimement contenir des espaces et de nombreux caractères visibles. Un nom d'en-tête ne le doit pas. Laisser un agent choisir des noms arbitraires crée plus d'ambiguïté que la plupart des passerelles d'actions n'en ont besoin.
Utilisez une règle prudente pour les noms : lettres ASCII, chiffres et trait d'union uniquement, avec une limite de longueur raisonnable. Rejetez les deux-points, les espaces, les caractères de contrôle et les octets non ASCII, sauf si vous avez une raison documentée de prendre en charge une extension précise. Le but n'est pas d'imiter chaque analyseur permissif sur Internet. Le but est de produire une seule requête sans ambiguïté.
Cette entrée doit échouer même si sa valeur apparente est ordinaire :
{
"name": "X-Trace\r\nAuthorization",
"value": "debug"
}
Celle-ci aussi :
{
"name": "X-Trace: injected",
"value": "debug"
}
Un deux-points appartient au moteur de rendu, pas au nom fourni. Si la passerelle l'accepte, un analyseur peut y voir un nom tandis qu'un autre y voit une ligne de champ complète. Un nom d'en-tête commençant par un espace invite à un désaccord similaire.
Les valeurs d'en-tête ont besoin de leur propre politique, au-delà de CRLF. Décidez si la passerelle accepte les en-têtes répétés, si elle autorise les virgules et quels en-têtes les agents peuvent remplacer. Ne fusionnez pas les noms en double en joignant du texte, sauf si la sémantique de l'en-tête l'autorise explicitement. Set-Cookie, Content-Length, Host, Authorization et les en-têtes de transfert méritent tous un traitement particulier ou une interdiction stricte. Je préfère les passerelles qui réservent entièrement les en-têtes de protocole et d'identifiants, puis proposent un petit ensemble d'en-têtes applicatifs que les agents peuvent fournir.
Cette recommandation suscite parfois des objections, car des en-têtes personnalisés arbitraires donnent à un outil HTTP générique une impression de souplesse. Cette souplesse plaît parce qu'elle évite d'ajouter une option à l'outil lorsqu'une API demande un en-tête de plus. Elle reste un mauvais choix par défaut pour le trafic d'agent authentifié. Une option nommée comme idempotency_key ou request_id a un format et un responsable clairement définis. Un ensemble d'en-têtes sans restriction n'a ni l'un ni l'autre.
Analyser les destinations une seule fois et garder leurs éléments structurés
Une destination n'est plus une chaîne une fois que la passerelle l'a acceptée. C'est une valeur structurée avec un schéma, un hôte, un port, un chemin, une requête et parfois des informations utilisateur ou un fragment. Analysez-la une fois avec un analyseur d'URL conforme aux standards, rejetez les composants interdits, puis transmettez la valeur analysée au client HTTP sans la reconvertir en chaîne de modèle.
La liste blanche exacte dépend de l'action. Un outil qui appelle l'API d'un seul fournisseur peut imposer HTTPS, un seul nom d'hôte et le port 443. Un outil plus large peut autoriser plusieurs origines préconfigurées. Dans les deux cas, rejetez les identifiants intégrés, les fragments, les noms d'hôte mal formés et tout retour chariot ou saut de ligne décodé avant de créer la requête. Les fragments ne partent pas sur le réseau, mais les accepter ajoute une confusion inutile dans les journaux et les écrans d'approbation.
Une frontière de requête utile ressemble à ceci :
input destination
-> decode once
-> reject CR and LF
-> parse URL
-> require HTTPS
-> require allowed host and allowed port
-> reject user info and fragment
-> pass parsed URL to HTTP client
L'ordre est intentionnel. Une liste blanche d'hôtes appliquée à une chaîne brute peut être trompée par des informations utilisateur comme https://[email protected]/. Un contrôle de saut de ligne appliqué seulement après qu'une bibliothèque a normalisé une valeur mal formée peut ne pas détecter ce que l'analyseur précédent a accepté. Analysez d'abord pour comprendre le sens, mais rejetez les caractères de contrôle interdits avant qu'un composant puisse les rendre comme de la syntaxe.
Ne construisez pas une requête HTTP en concaténant scheme + "://" + host + path. Ce modèle reconvertit une valeur structurée en texte au moment où vous avez le plus besoin des garanties de l'analyseur. Si une bibliothèque exige des arguments distincts pour l'hôte et le chemin, validez chaque argument avec la même règle sur les caractères de contrôle et utilisez l'API structurée de la bibliothèque.
Les encodages créent des contournements lorsque les couches divergent
Le CRLF littéral est le test dont tout le monde se souvient. Le CRLF encodé est le test qui révèle le véritable bug.
Un agent peut envoyer %0d%0a dans un composant d'URL. Si la passerelle contrôle la chaîne brute puis la décode comme URL avant de l'utiliser avec un moteur de rendu personnalisé, le contrôle a protégé la mauvaise représentation. Le saut de ligne apparaît après la validation. Le double encodage, comme %250d%250a, devient pertinent si différentes couches décodent séparément.
Choisissez une frontière de normalisation explicite. Par exemple, analysez le JSON une fois, décodez en pourcentage uniquement là où la norme URL l'exige, validez la représentation que votre moteur de rendu utilisera et interdisez tout décodage générique ultérieur. Gardez un test pour chaque transition. C'est moins séduisant qu'un grand outil de nettoyage, mais les personnes chargées de la revue peuvent le comprendre.
Une table de tests doit au minimum couvrir ces entrées pour chaque champ contrôlé par l'agent :
- un
\rlittéral, un\nlittéral et leur forme CRLF adjacente %0d,%0aet%0d%0alorsque l'encodage en pourcentage est accepté%250d%250asi un autre composant peut décoder plus tard- un nom d'en-tête contenant un deux-points ou un espace
- une destination avec des informations utilisateur, un port inattendu ou un hôte non approuvé
Ne testez pas seulement un message d'erreur. Vérifiez que le transport client ne s'exécute jamais pour une entrée rejetée. Un validateur qui signale une erreur après avoir construit ou mis en file d'attente la requête a déjà échoué à respecter la propriété de sécurité qui vous importe.
Il existe un piège similaire dans les formats de configuration. Les variables d'environnement, YAML, JSON et les arguments de shell ont chacun leurs propres règles d'échappement. Un dispositif de test qui contient visuellement \n peut contenir deux caractères inoffensifs, alors que le décodage JSON en production le transforme en saut de ligne. Construisez les tests en suivant le même chemin d'analyse que celui qui accepte les véritables appels d'agent.
Le problème se cache souvent dans le code de commodité
Le code dangereux arrive souvent après la revue de sécurité initiale. Quelqu'un ajoute des en-têtes personnalisés pour une nouvelle API, rend un proxy de débogage configurable ou écrit une fonction de nouvelle tentative qui reconstruit une requête pour la journalisation. Chaque modification paraît raisonnable. Ensemble, elles peuvent réintroduire le rendu par chaînes alors que la passerelle d'origine utilisait des API clientes sûres.
J'ai trouvé ce problème en suivant une action de l'entrée jusqu'au socket, et non en cherchant seulement le mot header. Cherchez l'interpolation de chaînes autour de Host:, Authorization:, Cookie:, Content-Length:, les cibles de requête, les commandes de proxy, les assistants de test HTTP brut et les utilitaires de relecture de journaux. Cherchez aussi les fonctions de décodage. Un validateur sûr près du point d'entrée sert peu si un chemin ultérieur accepte une représentation différente.
Un échec typique comporte quatre éléments. La passerelle valide l'hôte de destination avec une liste blanche. Elle stocke le reste de la destination sous forme de texte pour un client de bas niveau. Un assistant de nouvelle tentative décode ce texte en pourcentage pour rendre les journaux plus lisibles. L'assistant écrit ensuite une ligne de requête brute pour la nouvelle tentative, et %0d%0aX-Test:%20yes est devenu de la syntaxe de message.
Aucune de ces lignes ne dit « autoriser l'injection de requête ». Le défaut est l'invariant manquant : le contenu non fiable ne doit jamais contenir de caractères de saut de ligne lorsque le code rend un message HTTP. Placez cet invariant dans un validateur partagé et faites du rendu brut une exception que les tests doivent justifier.
Si vous utilisez une bibliothèque HTTP de haut niveau, gardez ce niveau d'abstraction jusqu'au bout. Évitez de revenir à un socket brut pour prendre en charge un seul point de terminaison inhabituel. Si une API exige réellement un format réseau inhabituel, donnez à cette intégration une implémentation dédiée et un modèle d'entrée étroit. N'affaiblissez pas le chemin générique pour elle.
Les journaux doivent montrer l'intention rejetée sans répéter les secrets
Lorsque la validation rejette une requête, enregistrez assez de contexte pour enquêter : heure, identité de la session ou du processus de l'agent, nom de l'action, catégorie de champ, motif du rejet et empreinte sûre ou longueur de la valeur fournie. Ne stockez pas la valeur complète de l'en-tête par défaut. Les en-têtes contiennent souvent des jetons, des données personnelles ou du matériel signé qui devient un autre problème de gestion des secrets une fois copié dans les journaux.
Distinguez l'événement d'audit d'une action tentée de celui d'une action exécutée. Une requête rejetée ne doit pas ressembler à un appel réseau en échec. Elle n'a jamais atteint le réseau. Cette distinction aide la réponse aux incidents et évite aux opérateurs de chercher un service distant qui n'a jamais vu la requête.
Sallyport conserve un journal Sessions pour les exécutions d'agent et un journal Activity pour les appels, tous deux projetés depuis son journal d'audit chiffré et chaîné par hachage. Une passerelle dispose ainsi d'un emplacement pertinent pour afficher une action refusée, mais la règle d'entrée doit toujours s'appliquer avant le rendu HTTP.
La propriété de vérification hors ligne compte ici. sp audit verify peut vérifier la chaîne sur du texte chiffré sans la clé du coffre-fort, ce qui permet d'établir que les événements enregistrés n'ont pas été modifiés. Il ne peut pas dire si un validateur était trop permissif. Le schéma d'événement et les tests doivent rendre visible la catégorie du champ rejeté sans conserver de données dangereuses.
Tester la requête sérialisée, pas seulement le validateur
Les tests unitaires de contains('\r') || contains('\n') sont nécessaires, mais insuffisants. Ils prouvent qu'une fonction rejette deux caractères. Ils ne prouvent pas que le transport réel ne peut pas recevoir un saut de ligne introduit par le décodage, les valeurs par défaut, les nouvelles tentatives ou un adaptateur personnalisé.
Utilisez un serveur de test local ou un transport d'enregistrement qui capture l'objet de requête sortant et, si votre pile le permet, ses octets sérialisés. Faites passer des entrées approuvées par le même point d'entrée d'outil que celui utilisé par les agents. Vérifiez la méthode, l'autorité, le chemin et l'ensemble complet des en-têtes. Injectez ensuite les cas rejetés et vérifiez que le transport d'enregistrement a vu zéro requête.
Une matrice de tests compacte détecte davantage de régressions qu'une longue collection de tests de sécurité vagues :
field input expected result
custom header value "ok\r\nX-Added: yes" reject before transport
custom header name "X-Mode: injected" reject before transport
destination path "/v1/a%0d%0aX-Added:%20yes" reject after decoding
allowed destination "https://api.example/v1/a" one request, expected host
Ajoutez des tests de propriétés si votre langage les rend pratiques. Générez des caractères de contrôle autour de chaque classe de caractères acceptée, puis vérifiez le rejet. Le fuzzing devient utile seulement lorsque le contrat est précis. Un fuzzer trouvera des cas étranges d'analyseur, mais ne peut pas dire si les noms d'en-tête arbitraires relevaient d'une décision produit ou d'un accident.
Lorsqu'un test échoue, inspectez les octets finaux avant de modifier le validateur. J'ai vu des équipes ajouter davantage de remplacements jusqu'à ce qu'un cas passe, pour découvrir ensuite que le client avait replié des espaces ou décodé deux fois. La bonne correction consiste généralement à supprimer le formateur ou le décodeur qui a rendu possible la syntaxe brute.
L'approbation humaine n'excuse pas les actions mal formées
Une approbation par session répond à la question de savoir si un processus d'agent donné peut agir. Une approbation par appel répond à la question de savoir si cette utilisation d'un identifiant protégé mérite une décision humaine. Aucune des deux ne rend une requête ambiguë sûre. Une personne ne peut pas inspecter de façon fiable des caractères de contrôle cachés dans une longue destination ou valeur d'en-tête, et la fatigue d'approbation transformera une invite subtile en simple clic d'acceptation.
Gardez aussi les données d'approbation structurées. Affichez un hôte normalisé, la méthode, le chemin, les catégories d'en-têtes nommées et une indication claire lorsque l'action ajoute un identifiant. Rejetez les entrées mal formées avant que la carte d'approbation apparaisse. Demander à une personne d'approuver une requête invalide déplace un bug d'analyseur dans une interface utilisateur.
La barrière de coffre-fort, l'autorisation de session et le paramétrage de clé par appel de Sallyport offrent différents moments de contrôle humain. La frontière d'action HTTP a toujours besoin d'un moteur de rendu strict, car la décision d'approbation doit couvrir une action bien formée, et non une chaîne dont le sens change après le clic.
Le premier changement concret n'est pas une vaste réécriture. Repérez chaque chemin qui accepte du texte de destination ou des en-têtes personnalisés contrôlés par un agent, ajoutez un rejet CRLF inconditionnel avant le rendu et écrivez un test de transport qui prouve qu'une entrée rejetée n'envoie rien. Supprimez ensuite le code de commodité qui reconstruit HTTP avec des chaînes. C'est là que ce défaut revient sans cesse.
FAQ
Dois-je rejeter les séquences CRLF dans les valeurs d'en-tête HTTP ?
Rejetez les deux caractères partout où ils peuvent entrer dans la syntaxe d'une requête : chaînes de destination, noms et valeurs d'en-tête, remplacements de méthode, fragments de cookie et paramètres de proxy. Traitez les données décodées après encodage en pourcentage de la même manière. La validation doit avoir lieu avant toute interpolation, analyse ou appel de bibliothèque susceptible de normaliser l'entrée.
Une bibliothèque HTTP sûre peut-elle empêcher le request smuggling ?
Une bibliothèque cliente HTTP classique aide, mais ne dispense pas de valider à la frontière. Votre passerelle peut décoder, combiner ou formater l'entrée de l'agent avant qu'elle atteigne la bibliothèque, et certaines entrées affectent d'abord un autre analyseur. Rejeter les sauts de ligne à la frontière rend le contrat attendu explicite.
Pourquoi les retours chariot sont-ils dangereux dans les requêtes API ?
Non. Les sauts de ligne séparent la syntaxe des messages HTTP. Les autoriser dans une destination ou un champ d'en-tête peut transformer des données en un autre champ ou en un autre élément de requête. Le code vulnérable peut tout de même envoyer une requête syntaxiquement valide, ce qui explique pourquoi ce bug peut passer inaperçu lors de tests ordinaires.
Un CRLF encodé en pourcentage peut-il contourner la validation ?
L'analyseur doit décoder une fois, puis valider la valeur décodée. S'il accepte un saut de ligne encodé en pourcentage et qu'une couche ultérieure le décode, le contrôle a eu lieu trop tôt. Gardez les valeurs de requête décodées et rendues séparées afin que le code ultérieur ne les décode pas à nouveau.
Comment valider les noms d'en-tête personnalisés ?
Les noms d'en-tête doivent suivre une petite liste blanche, par exemple lettres, chiffres et trait d'union, puis tout le reste doit être rejeté. Cela évite les sauts de ligne, l'insertion de deux-points, les manipulations avec des espaces et les noms que différents composants interprètent différemment. N'acceptez pas des noms d'en-tête arbitraires fournis par l'agent au seul motif que la valeur est contrôlée.
L'analyse d'URL suffit-elle à sécuriser une destination d'agent ?
Non. Un analyseur d'URL peut indiquer où se situent les limites de l'hôte et du chemin, mais il ne peut pas décider si une destination est autorisée. Analysez d'abord l'URL, puis appliquez vos propres règles sur le schéma, l'hôte, le port, les informations utilisateur, les fragments et les caractères de contrôle décodés.
Que doit faire une passerelle d'actions lorsqu'elle rencontre un saut de ligne ?
Rejetez-la avant de construire le moindre objet de requête. Ne la supprimez pas avec trim(), ne remplacez pas le saut de ligne par une espace et ne l'effacez pas silencieusement. Ces corrections masquent l'action d'origine dans les journaux et peuvent modifier le sens d'une entrée signée ou authentifiée. Renvoyez une erreur qui identifie le champ sans répéter un secret.
La validation doit-elle avoir lieu avant ou après le décodage d'URL ?
Contrôlez l'entrée brute, puis sa représentation décodée une fois, puis les octets finaux sérialisés dans les tests. Le premier contrôle détecte les entrées manifestement dangereuses, le contrôle après décodage bloque les contournements par encodage, et le test de sérialisation détecte le code de formatage non sûr. Un seul contrôle laisse la possibilité à une transformation ultérieure de modifier la valeur.
L'injection d'identifiants peut-elle provoquer une injection de requête ?
Même si l'identifiant est ajouté par du code de confiance, la requête qui l'entoure peut rester contrôlée par un agent. Une valeur d'en-tête hostile peut placer un second en-tête à côté de l'identifiant injecté, tandis qu'une destination hostile peut rediriger la requête authentifiée. Protéger le secret et protéger la forme de la requête sont deux tâches distinctes.
Quels cas de test détectent l'injection d'en-tête HTTP ?
Testez chaque position d'entrée acceptée avec un retour chariot littéral, un saut de ligne littéral, CRLF, des formes encodées en pourcentage, des formes doublement encodées, des espaces en tête et un deux-points dans un nom d'en-tête. Vérifiez que la passerelle rejette chaque cas avant toute E/S réseau. Inspectez ensuite une requête capturée pour les cas autorisés et vérifiez qu'elle contient exactement les champs prévus.