Encodage aux limites de sortie
Traitez tout ce que le modèle émet comme une entrée hostile pour tout ce qui la consomme. Encodez et validez au niveau de chaque destination — moteur de rendu, shell, requête, agent en aval — en utilisant les règles propres à cette destination. Un outil d'assainissement global unique ne peut pas faire cela : un échappement correct pour HTML n'a aucun sens pour un shell.
Définition
L'encodage aux limites de sortie consiste à appliquer un encodage et une validation spécifiques à la destination sur la sortie du modèle à chaque point de passage vers un système qui l'interprétera, plutôt que de la filtrer une seule fois de manière générique à la sortie du modèle. Son absence constitue la vulnérabilité que l'OWASP qualifie de traitement incorrect des sorties (improper output handling).
Problème
Les équipes sécurisent le côté entrée contre l'injection de requêtes (prompt injection) et laissent le côté sortie ouvert. Le texte du modèle atteint alors un moteur de rendu, un shell, une base de données ou le contexte d'un autre agent, où il est interprété comme une instruction plutôt que d'être affiché comme une donnée. L'attaquant n'a jamais besoin d'atteindre directement le modèle.
Quand l'utiliser
Tout agent dont la sortie atteint un élément qui l'analyse : une interface de chat affichant du markdown, un agent de codage exécutant une commande suggérée, un appel d'outil construit à partir d'arguments générés, un résumé injecté dans l'invite d'un second agent, ou un webhook qui transmet le texte plus loin.
Solution
Énumérez les destinations avant d'écrire le moindre filtre. Chaque endroit où la sortie du modèle atterrit et est interprétée — moteur de rendu HTML, shell, requête, chemin de fichier, récupérateur d'URL, contexte d'un autre agent, webhook en aval — constitue une limite distincte avec des règles distinctes.
Encodez à la destination, pas à la source. Échappez le HTML pour le moteur de rendu, paramétrez pour la requête, passez un tableau `argv` au processus. L'encodage doit se faire là où l'interprétation a lieu, car c'est seulement à cet endroit que vous savez ce qui sera interprété.
Privilégiez les sorties structurées plutôt que de la prose que vous devez analyser a posteriori. Un appel d'outil avec des arguments typés dispose d'un schéma de validation ; une phrase dans laquelle vous recherchez un nom de fichier par expression régulière n'en a pas.
Validez la valeur, pas seulement la syntaxe. Un chemin encodé reste un chemin : vérifiez qu'il se résout bien à l'intérieur du répertoire prévu avant de l'ouvrir.
Traitez les URL sortantes comme une destination à part entière. Les images et les liens affichés provoquent une requête de récupération sans clic, exfiltrant ainsi des données ; autorisez uniquement (via une liste d'autorisation) les hôtes qu'un lien affiché peut atteindre.
Faites de la limite l'unique voie de passage. Si un chemin de code peut consommer la sortie brute du modèle sans passer par un encodeur de destination, le contrôle est indicatif plutôt que réel.
Composants
Avantages
- Brise la chaîne d'injection là où cela compte : même un modèle entièrement manipulé ne peut pas faire agir un système en aval, car ce système n'interprète jamais son texte.
- Indépendant du comportement du modèle. Cela continue de fonctionner malgré les mises à niveau du modèle, les nouveaux jailbreaks et les modifications d'invites, car cela ne dépend pas du refus du modèle.
- Testable. Chaque destination a une charge utile et un résultat de réussite ou d'échec, de sorte que le contrôle produit des preuves plutôt que de simples assurances.
- Peu coûteux s'il est appliqué tôt. Ajouter un encodeur est une modification de limite ; en intégrer un après coup, une fois que la destination est partout, relève de la refonte.
Risques
- Un seul outil d'assainissement pour toutes les destinations. Cela donne l'impression d'un contrôle, valide la liste de contrôle, mais s'avère incorrect à chaque limite, sauf celle pour laquelle il a été écrit.
- Un encodage qui altère le produit : un sur-échappement transforme le markdown légitime, les blocs de code et le texte non latin en bruit, et la pression pour l'assouplir pèse sur l'encodeur plutôt que sur la liste des destinations.
- Un inventaire des destinations qui vieillit. Les nouvelles intégrations ajoutent des consommateurs, et rien ne signale d'erreur lorsqu'un consommateur est oublié.
- Confondre la détection et l'encodage. Analyser la sortie à la recherche de chaînes suspectes permet de détecter les charges utiles de l'année dernière ; l'encodage n'a pas du tout besoin de reconnaître l'attaque.
Quand ne pas l'utiliser
- Une sortie qui n'est jamais interprétée — un score, une énumération, un booléen comparé par l'appelant. Contraignez plutôt le type ; un encodeur sur un ensemble de valeurs fermé est superflu.
- Des outils mono-utilisateur entièrement locaux, sans rendu ni exécution de processus, où le seul consommateur est une personne lisant du texte.
- Lorsque la destination applique déjà un paramétrage par construction, comme une liaison ORM ou un moteur de template qui échappe par défaut. Un second encodeur n'apporte rien et peut provoquer un double encodage.
Technologies
Exemples
- Un agent de support résume un ticket dont le corps contient une image markdown pointant vers l'hôte d'un attaquant. La console l'affiche, le navigateur récupère l'URL et la conversation est divulguée sans que personne n'ait cliqué sur quoi que ce soit. Désactiver le HTML brut et autoriser uniquement certains hôtes d'images permet de résoudre ce problème.
- Un agent de codage propose une commande shell. L'exécuteur transmet la chaîne à un shell, de sorte qu'un nom de fichier contenant un séparateur de commande est exécuté. Passer un tableau `argv` à la place supprime complètement l'étape d'analyse du shell.
- Le résumé d'un agent est placé dans l'invite d'un second agent. Le résumé contenait des instructions, et le second agent les a suivies. Encadrer la zone non approuvée et la marquer comme donnée constitue la limite dans ce cas.
KPI
- Couverture des destinations
- Part des consommateurs connus de la sortie du modèle disposant d'un encodeur à la limite. En dessous de 100 %, le contrôle présente une faille spécifique, et nommer la destination est plus utile qu'un pourcentage qui masque la réalité par une moyenne.
- Taux de neutralisation des charges utiles
- Part des charges utiles de test par destination neutralisées à la limite. L'objectif est de 100 % : tout autre résultat désigne une destination à corriger plutôt qu'un chiffre à améliorer.
- Délai de couverture d'une nouvelle destination
- Temps écoulé entre la mise en production d'une nouvelle intégration et la création de son encodeur. Permet de mesurer si l'inventaire suit l'évolution du produit plutôt que de savoir s'il a été correct à un instant T.
- Volume de déclenchement aux limites
- Fréquence à laquelle les encodeurs neutralisent un élément en production. Un zéro absolu signifie généralement que l'encodeur n'est pas sur le chemin, et non qu'aucun élément hostile n'arrive.
Modes de défaillance observés
- Exfiltration silencieuse via un balisage affiché : une image ou un lien provoque une requête de récupération sans clic, de sorte que les données s'échappent sans action de l'utilisateur et sans erreur visible.
- Injection de second ordre : une sortie encodée correctement pour la console est stockée puis affichée ailleurs — un visualiseur de journaux, un ticket, un e-mail récapitulatif — là où cet encodage ne s'applique pas.
- L'encodeur qui ne se trouve que sur le chemin nominal (happy path). Les branches d'erreur, les tentatives et les solutions de repli émettent le même texte via un chemin de code différent dépourvu de tout contrôle.
- Double encodage. Deux couches effectuent chacune un échappement correct, les utilisateurs voient des entités échappées dans le produit, et la correction supprime la mauvaise couche.
Leçons apprises
- Énumérez les destinations avant d'écrire des filtres. Presque chaque défaillance réelle ici provient d'un consommateur que personne n'a répertorié, et non d'un encodeur mal écrit.
- La destination que vous oubliez est rarement un écran. Il s'agit d'un webhook, d'un visualiseur de journaux, d'un export ou d'un e-mail récapitulatif — un endroit où la sortie est envoyée sans que personne ne l'assimile à un rendu.
- L'encodage l'emporte sur la détection car il n'a pas besoin de reconnaître l'attaque. Une liste de blocage de charges utiles n'est qu'une description des attaques déjà publiques.
- Ne laissez pas une unique fonction `sanitise()` devenir la solution. Son nom suggère l'exhaustivité alors que son comportement n'est correct que pour une seule et unique destination.
FAQ
- N'est-ce pas la même chose que de filtrer les entrées contre l'injection de requêtes (prompt injection) ?
- Non — ils défendent des extrémités opposées. Le filtrage des entrées tente d'empêcher le modèle d'être manipulé, ce qui dépend du modèle. Cette approche suppose que la manipulation a déjà réussi et empêche l'exécution de la sortie, ce qui ne dépend pas du tout du modèle. Un système doté uniquement de la première protection échoue dès qu'un nouveau jailbreak apparaît.
- Ne s'agit-il pas simplement d'un assainissement des sorties ? Pourquoi ne pas utiliser un seul outil d'assainissement pour tout ?
- Parce que l'encodage est contextuel. Échapper un guillemet protège un moteur de rendu HTML mais ne fait rien pour un shell ; l'échappement pour shell protège un shell mais corrompt le texte affiché. Une fonction partagée doit choisir un contexte unique, s'avère incorrecte dans les autres, et donne l'illusion d'une couverture tout en constituant un point de défaillance unique.
- Le modèle nous appartient et l'invite est fixe. En avons-nous tout de même besoin ?
- Oui, si un contenu lu par le modèle provient de l'extérieur : une page récupérée, un fichier utilisateur, un résultat d'outil, un enregistrement de mémoire. L'instruction n'a pas besoin d'arriver via votre prompt — elle arrive via tout ce que le modèle lit, et le fait que votre prompt soit fixe ne limite pas cela.