Aller au contenu
Lecture : 0 %

Pourquoi aucun filtre n'arrête l'injection de prompt

Pourquoi un détecteur probabiliste perd contre un attaquant qui relance autant qu'il veut, ce qu'un filtre apporte malgré tout, et où placer la défense qui tient : dans ce que l'agent a le droit de faire.

Par Elias Varen7 min de lecture

Soit un filtre d'injection qui bloque 99 % des tentatives. C'est un excellent filtre, et la plupart n'en sont pas là. L'attaquant, lui, a toujours le droit de relancer. La probabilité qu'une tentative donnée passe est de 0,01 ; celle qu'au moins une passe sur deux cents essais est de 1 − 0,99²⁰⁰, soit environ 87 %. Deux cents essais, pour un script, représentent quelques minutes.

Ce calcul est encore trop généreux pour le défenseur, parce qu'il suppose des essais indépendants. Un attaquant réel apprend de chaque refus. Il ne tire pas deux cents fois au hasard ; il cherche la zone où le filtre est faible, puis il y reste.

En classification, 99 % est un bon résultat. En sécurité, cela donne une porte qui s'ouvre une fois sur cent à qui frappe assez longtemps.

Pas de requête paramétrée pour un modèle de langage

L'injection SQL est un problème réglé, et on ne le doit pas à de meilleurs filtres sur les chaînes de caractères. Les requêtes paramétrées séparent structurellement le code des données : la valeur passée en paramètre ne peut pas devenir du SQL, quel que soit son contenu. La garantie se trouve dans le protocole, sans aucune inspection.

Un modèle de langage n'a pas cette séparation. Le prompt système, le message de l'utilisateur, le document retrouvé, le résultat d'un outil arrivent tous dans la même séquence de tokens, et le modèle a été entraîné à suivre ce qui ressemble à une consigne. Les rôles (system, user, résultat d'outil) et les délimiteurs qu'on ajoute autour d'un contenu sont des indications que le modèle respecte la plupart du temps. Or « la plupart du temps » décrit une propriété statistique, qui ne fait pas une frontière.

Tant que cette frontière n'existe pas dans l'architecture des modèles, toute défense placée au niveau du texte est probabiliste par construction. On peut déplacer le taux, sans jamais le mettre à zéro.

Pourquoi l'attaquant adaptatif gagne la course

Un filtre, qu'il soit une liste de motifs, un classifieur dédié ou un second modèle chargé de juger, apprend une distribution, celle des attaques connues au moment où on l'a construit. Plusieurs asymétries jouent contre lui.

L'espace des formulations est ouvert. Une même intention s'écrit d'une infinité de manières : autre langue, reformulation indirecte, consigne répartie sur plusieurs fragments qui ne prennent sens qu'une fois réunis dans le contexte, texte présenté comme une citation, un exemple ou un message d'erreur. Le filtre voit des surfaces, alors que le modèle cible comprend le sens. Tout écart de compréhension entre le filtre et le modèle protégé devient un passage.

Le filtre est lui-même attaquable. Si votre détecteur est un modèle de langage, le contenu qu'il inspecte peut s'adresser à lui. On a ajouté une couche qui souffre du défaut qu'elle devait corriger.

L'attaquant voit le verdict. Dans la plupart des produits, un blocage est observable (la réponse change, un message d'erreur apparaît, l'action n'a pas lieu), et chaque refus lui donne un bit d'information. Le défenseur ne voit que les attaques qu'il a détectées. Celles qui passent ne laissent pas de trace dans son tableau de bord, ce qui explique justement que son taux mesuré soit flatteur.

Ce qu'un filtre apporte quand même

Je ne retire pas les filtres. Je leur retire seulement le rôle de garantie.

Un filtre élimine le bruit, c'est-à-dire les tentatives recopiées, les attaques opportunistes non ciblées, les contenus piégés à grande échelle sans connaissance de votre système. C'est le gros du volume, et l'arrêter a une valeur réelle.

Il augmente aussi le coût de l'attaque. L'attaquant ciblé doit investir du temps, et ce temps produit des essais ratés, donc du signal. Un pic de blocages sur un tenant, une adresse ou un document donné est une alerte utile, à condition que quelqu'un la regarde.

Le filtre a enfin un prix. Un appel de classification avant chaque appel d'outil ajoute de la latence sur chaque étape de l'agent. Les faux positifs frappent les utilisateurs légitimes, car sur un produit technique, un ticket qui cite un prompt, un document qui parle de sécurité ou un extrait de code avec des commentaires impératifs ressemblent à des injections. Avec 1 % de faux positifs sur un assistant qui traite 5 000 documents par jour, on obtient cinquante blocages injustifiés quotidiens, autant d'utilisateurs qui ne comprennent pas pourquoi rien ne marche. La pression pour desserrer le seuil arrive vite, et elle vient du support bien plus que de l'attaquant.

Borner ce que l'agent a le droit de faire

Puisque l'injection finira par passer, la conception part de l'hypothèse qu'elle est passée et borne ce qui s'ensuit. On raisonne comme pour un processus compromis, où l'on compte sur les droits du processus plutôt que sur l'antivirus.

Les protections qui ne dépendent pas du comportement du modèle sont connues :

  • Les droits des outils. L'outil de lecture ne voit que les lignes du tenant courant, par une contrainte appliquée dans la requête et non dans le prompt. Un agent détourné qui demande les données d'un autre tenant reçoit zéro ligne.
  • La liste des outils. Un agent qui traite du contenu non fiable n'a pas d'outil d'envoi. Ce qui n'est pas câblé ne peut pas être appelé, quelle que soit la qualité de l'injection.
  • Les canaux de sortie. Pas de chargement d'image distante dans le rendu, pas d'appel HTTP vers un domaine arbitraire, liste d'autorisation au niveau réseau.
  • La confirmation humaine pour les actions irréversibles, présentée avec les paramètres réels de l'action et non avec le résumé qu'en fait le modèle.
  • Le marquage de provenance. Dès qu'un contenu non fiable est entré dans le contexte, la session est considérée comme contaminée, et certains outils se ferment pour le reste de la session.

Aucune de ces mesures n'est probabiliste. Elles échouent par erreur de configuration, et une erreur de configuration se teste, alors que l'ingéniosité de l'adversaire ne se teste pas.

Le coût est connu, puisque chacune retire de l'utilité. Un agent contraint fait moins de choses, demande plus souvent, et le produit paraît moins magique en démonstration. L'arbitrage se négocie avec le produit, et aucun fournisseur de garde-fous ne le fera à sa place.

Quand le filtre seul suffit

Dans certains cas, je me contente d'un filtre, voire de rien.

Si le modèle n'a aucun outil et que sa sortie est affichée en texte brut à la personne qui a fourni l'entrée, une injection réussie produit une réponse fausse ou étrange pour l'attaquant lui-même. Il n'y a ni donnée tierce ni action. Le risque est de réputation, et un filtre de contenu en sortie le couvre correctement.

Si le contenu traité est entièrement de première main, rédigé par votre équipe et versionné, le contenu non fiable est absent. L'injection suppose un attaquant qui écrit quelque part ; s'il n'a nulle part où écrire, le filtre protège contre une menace qui n'existe pas.

Dans tous les autres cas, dès que le contenu d'un tiers rencontre des outils, le filtre reste une couche d'observation.

Supposer que l'attaque est passée

Quand quelqu'un propose un garde-fou comme réponse à l'injection de prompt, je pose en revue une seule question : « supposons que le filtre a laissé passer l'attaque ; que se passe-t-il ensuite ? »

  • Si la réponse est « rien de grave, l'agent n'a ni la donnée ni le canal », la conception est saine et le filtre est un bonus.
  • Si la réponse est « ça dépend de ce que le modèle décide », la sécurité repose sur une probabilité, et il faut retirer une capacité.
  • Si la réponse est « ça ne passera pas », la revue n'est pas terminée.

Un filtre peut réduire la fréquence d'un incident, mais pas sa gravité maximale, qui se décide dans la liste des outils et dans leurs droits. On commence donc par là, et le filtre vient ensuite pour ce qu'il sait faire, nettoyer le bruit et signaler que quelqu'un cherche.

Pourquoi aucun filtre n'arrête l'injection de prompt · Deepstack