Du modèle à l'agent
Un modèle de langage seul ne fait que produire du texte. On l'appelle agent dès qu'on lui donne des outils : envoyer un courriel, lire un fichier, appeler une API, exécuter une requête. Chaque outil ajouté est une capacité d'agir sur le monde, donc un droit. Et la somme de ces droits, c'est la surface d'attaque de l'agent.
L'analogie avec l'injection SQL est utile : dans les deux cas, des données fournies par un tiers finissent par être interprétées comme des instructions. Mais la différence est de fond : SQL possède une grammaire formelle et des requêtes paramétrées qui séparent proprement code et données, alors qu'un LLM ne dispose d'aucune frontière syntaxique fiable entre instruction et contenu. Et ici, l'instruction ne modifie plus une requête, elle actionne un outil. L'enjeu dépasse donc le modèle lui-même : ce qui compte, c'est ce que le modèle a le droit de faire.
Le modèle de menace
La menace propre à un agent est le député confus (confused deputy) : un composant privilégié qu'on amène à agir pour le compte d'un tiers non autorisé. L'agent a des droits ; un contenu non fiable qu'il lit peut le pousser à s'en servir.
Ce contenu non fiable n'a pas besoin de venir de l'utilisateur. Il peut arriver par tout ce que l'agent lit pour lui : une page web résumée, un courriel traité, un document indexé, un ticket, un commentaire dans du code, la réponse d'une API. C'est l'injection indirecte, et sa victime n'est pas l'attaquant mais la personne dont l'agent porte les droits. L'OWASP place l'injection de prompt en tête de son Top 10 des applications LLM précisément à cause de cette portée.
Les surfaces
On les cartographie une à une.
- Le contexte. Tout ce qui entre dans la fenêtre du modèle (prompt système, historique, documents récupérés, sorties d'outils) est lu comme un tout, et le modèle décide seul de ce à quoi il obéit.
- Les outils. Chaque outil est une porte. Sa dangerosité se mesure à ses effets : lire est moins grave qu'écrire, écrire moins qu'exécuter ou payer.
- La mémoire. Un agent qui retient l'entre-deux des sessions peut être empoisonné une fois pour agir plus tard : l'instruction déposée aujourd'hui se déclenche demain.
- La récupération (RAG). La base documentaire est un canal d'entrée. Qui peut y écrire peut y déposer des instructions qui atteindront l'agent.
- Le multi-agent. Quand des agents s'appellent entre eux, la sortie de l'un est l'entrée de l'autre, et une injection se propage de proche en proche.
Le rayon d'impact, ce sont les outils
Un agent sans outils qui se fait injecter produit, au pire, une mauvaise réponse ou divulgue son contexte. Le même agent doté d'outils hérite, au moment où il suit une instruction injectée, de tous les droits de ces outils. D'où la règle : le rayon d'impact potentiel d'une injection est largement déterminé par les outils et les privilèges qu'elle peut atteindre ; les données accessibles, les contrôles en place et les canaux d'exfiltration font le reste.
L'exfiltration passe souvent par des canaux discrets : selon le client, son moteur de rendu et ses politiques réseau, un lien ou une image qu'il affiche automatiquement peut suffire à faire partir des données dans ses paramètres, sans le moindre clic. La parade tient à l'architecture, pas au texte : on désactive le rendu automatique des ressources externes, ou on le restreint à une liste d'autorisation.
Permissions et moindre privilège
La question n'est pas « comment empêcher l'agent d'être trompé ? » (on ne le peut pas entièrement), mais « que peut-il faire au pire s'il l'est ? ». Un outil se conçoit borné : les droits minimaux pour la tâche, et rien de plus.
# À éviter : un outil qui prend une requête libre et l'exécute. Sa portée
# est totale, donc celle d'une injection l'est aussi.
def executer_sql(requete: str) -> list[dict]:
return db.execute(requete).fetchall()
# À préférer : une capacité étroite, en lecture seule, sur une seule table,
# paramétrée. Une injection ne peut pas en sortir.
def chercher_commande(numero: str) -> dict | None:
ligne = db.execute(
"SELECT id, statut, total FROM commandes WHERE numero = ?",
(numero,),
).fetchone()
return dict(ligne) if ligne else None
Pour tout effet de bord (envoi, paiement, suppression, modification de droits), on interpose une confirmation humaine. L'agent propose, une personne dispose.
EFFETS_DE_BORD = {"envoyer_courriel", "payer", "supprimer", "modifier_droits"}
def appliquer(outil: str, args: dict):
# Un contenu non fiable ne doit jamais déclencher seul un acte
# irréversible : l'agent prépare, l'humain valide.
if outil in EFFETS_DE_BORD:
return demander_confirmation_humaine(outil, args)
return OUTILS[outil](**args)
Tester un agent
Éprouver un agent, dans un mandat autorisé, suit une méthode en cinq temps :
- Cartographier les entrées. Tout ce qui entre dans le contexte, et d'où cela vient.
- Repérer le non fiable. Parmi ces entrées, celles qu'un tiers peut contrôler : web, courriels, documents indexés, réponses d'API.
- Inventorier les outils. Leur liste, leurs droits réels, leurs effets. C'est la carte du pire cas.
- Éprouver chaque canal de sortie. Liens, images, appels réseau, écritures de fichiers : par où des données peuvent-elles sortir ?
- Traiter le prompt système comme public. Supposer qu'il sera extrait, et vérifier que la sécurité n'en dépend pas.
Ce qu'on cherche, ce ne sont pas des formules magiques qui « jailbreakent » le modèle, mais des chemins : une entrée non fiable qui atteint un outil privilégié. Trouver ce chemin, c'est trouver la vulnérabilité.
Concevoir pour contenir
Les architectures les plus robustes séparent les rôles. Un motif récurrent oppose un modèle privilégié, qui peut actionner les outils mais ne lit jamais de contenu non fiable, à un modèle en quarantaine, qui lit le contenu non fiable mais ne peut renvoyer que des données structurées et contraintes, jamais des instructions. La frontière entre les deux est explicite, et c'est elle qu'on audite.
Le reste relève de l'hygiène : des jetons de capacité à portée réduite plutôt que des clés générales, un bac à sable pour tout code généré, une journalisation de chaque appel d'outil avec le contexte qui l'a provoqué. Un agent bien conçu n'est pas un agent qu'on ne peut pas tromper : c'est un agent dont on a borné à l'avance ce qu'il peut faire une fois trompé.
À retenir
- 01
Donner un outil à un agent, c'est lui donner un droit : la surface, ce sont les outils.
- 02
L'impact potentiel d'une injection dépend d'abord des outils et des droits qu'elle peut atteindre.
- 03
Le moindre privilège et la confirmation humaine bornent les dégâts, pas le prompt.
- 04
Tester un agent, c'est cartographier ses outils, ses droits et ses canaux de sortie.

