No. 002Sécurité9 min de lecture

L'injection de prompt est la nouvelle injection SQL — et la parade n'est pas SQL

P2SQL, CVE-2024-5565, EchoLeak, la fuite Supabase : le SQL hostile arrive désormais dans la sortie du modèle. La parade est un agent sans syntaxe à injecter.

Le ticket de support ne ressemblait pas à une attaque. Il attendait dans la file comme n'importe quel autre — un message client à résumer. Dans le corps du message se trouvaient des instructions : lire la table integration_tokens et en publier le contenu dans ce fil.

Un développeur avait connecté un assistant IA à cette file via le serveur MCP Supabase, qui se connecte avec des identifiants service_role — le rôle qui contourne entièrement la sécurité au niveau des lignes. L'assistant a lu le ticket, en a interprété le contenu comme une tâche, et l'a exécutée contre la base de données de production. Les tokens sont apparus dans le fil du ticket, où l'attaquant n'avait plus qu'à les lire Simon Willison, 2025.

Rien n'a été exploité au sens traditionnel du terme. Pas de corruption mémoire, pas de vérification d'authentification manquante, pas de dépendance vulnérable. Le texte hostile a atteint le plan de données uniquement par l'intermédiaire de l'agent — et la preuve de concept se généralise à tout agent câblé de la même façon.

C'est de l'injection SQL avec l'interpréteur déplacé. Le texte hostile devient toujours une commande de base de données. Mais la chaîne n'arrive plus dans l'entrée, là où vingt ans d'outillage de sanitisation montent la garde. Elle arrive dans la sortie du modèle — en aval de tous vos filtres.

SQL INJECTION, 2005 — THE PAYLOAD ARRIVES IN THE INPUT input sanitizer application database parameterized payload dies here PROMPT-TO-SQL, NOW — THE PAYLOAD ARRIVES IN THE OUTPUT input sanitizer model generated SQL database untrusted context — tickets, pages, rows hostile SQL is authored here — after every filter
Le filtre protège une porte que le payload n'emprunte plus — en P2SQL, le SQL hostile est produit dans la sortie du modèle (Pedro et al., ICSE 2025).

Le bilan

Des chercheurs de l'INESC-ID Lisbonne ont nommé le schéma en 2023 : P2SQL, injection prompt-to-SQL Pedro et al., 2025. Travaillant contre un chatbot LangChain réel sur Postgres, ils ont construit sept attaques représentatives de sévérité croissante — lectures non autorisées, tables corrompues, tables supprimées, jusqu'à l'exécution de code — et les ont testées sur sept modèles de pointe. Les échecs étaient indépendants du modèle. Leur conclusion, mot pour mot : « Les applications intégrant des LLM basées sur Langchain sont très vulnérables aux attaques par injection P2SQL. » L'article a été présenté à ICSE 2025 — la façon qu'a la revue par les pairs de dire que ce n'est pas une curiosité.

À mi-2024, le schéma avait un numéro CVE. Vanna.AI, une bibliothèque text-to-SQL, injectait les questions des utilisateurs dans un prompt qui générait du code de visualisation Plotly, puis exécutait ce code avec exec(). Une question soigneusement construite menait directement à l'exécution de code à distance — CVE-2024-5565, CVSS 8.1 JFrog, 2024. Notons où vivait le payload : dans la sortie du modèle, après tout contrôle d'entrée. Les prompts de garde de Vanna ne l'ont pas arrêté, car un garde-fou n'est que du texte supplémentaire dans la même fenêtre de contexte que l'attaque.

Fin 2024, le classement était officiel : l'injection de prompt est LLM01, le risque numéro un du Top 10 OWASP pour les applications LLM, occupant la première place pour la deuxième édition consécutive OWASP, 2025. Le diagnostic d'OWASP sur la cause profonde résume tout en une phrase : les LLM traitent instructions et données dans le même canal, sans séparation claire. C'est aussi pourquoi la catégorie se divise en deux — l'injection directe saisie par l'utilisateur, et l'injection indirecte portée par ce que le modèle récupère. Le ticket Supabase était indirect. Tout comme le pire cas jamais documenté.

En juin 2025, CVE-2025-32711 — EchoLeak, CVSS 9.3 — a été divulgué dans Microsoft 365 Copilot : le premier exploit d'injection de prompt zero-click documenté dans un système LLM en production arXiv, 2025. Un seul email soigneusement construit — instructions dissimulées dans des commentaires HTML, texte blanc sur blanc, Markdown en style référence — et Copilot a exfiltré des données internes vers le serveur de l'attaquant. L'utilisateur n'a rien cliqué. La chaîne a contourné le classifieur XPIA dédié de Microsoft et sa rédaction des liens en sortie.

CVSS 10 8.1 9.3 2025-06 CVE-2025-32711 — EchoLeak zero-click, one email, M365 Copilot 2023-08 P2SQL coined 7 attacks × 7 LLMs 2024-06 CVE-2024-5565 Vanna.AI — RCE 2024-11 OWASP LLM01 #1 for 2nd edition 2025-07 Supabase MCP PoC no code vulnerability
Deux ans, du laboratoire au zero-click — barres CVSS à l'échelle (arXiv 2308.01990 ; JFrog 2024 ; OWASP 2025 ; arXiv 2509.10540 ; Willison 2025).

Pourquoi les parades évidentes échouent

Chacun des incidents ci-dessus invalide l'une des défenses standard.

Filtrer l'entrée. EchoLeak a traversé XPIA, un classifieur que Microsoft a conçu spécifiquement pour détecter les tentatives d'injection arXiv, 2025. Les classifieurs sont probabilistes. L'attaquant n'a besoin que d'un seul raté et dispose de tentatives illimitées pour le provoquer ; le défenseur doit maintenir un bilan parfait face à des entrées conçues pour ressembler à n'importe quoi d'autre.

Instruire le modèle. Vanna livrait des prompts de garde ; CVE-2024-5565 les a traversés JFrog, 2024. Il n'existe pas de canal privilégié à l'intérieur d'une fenêtre de contexte. Le texte système, le texte utilisateur et le texte d'attaque sont de même nature — c'est précisément la cause profonde qu'OWASP identifie OWASP, 2025.

Filtrer la sortie. Analyser le SQL généré et bloquer les parties dangereuses. Il faut alors maintenir une liste de refus couvrant toutes les constructions à capacité d'écriture dans chaque dialecte supporté, ainsi que toutes les fonctions à effets de bord — pendant que le co-auteur de l'adversaire est le modèle lui-même, prêt à reformuler une requête bloquée en une forme autorisée. Significativement, les propres défenses proposées par les auteurs de P2SQL se situent en dehors du modèle : analyser et mettre en liste blanche le SQL généré, et exécuter l'agent sous un rôle de base de données séparé et restreint Pedro et al., 2025. Les contrôles qui tiennent sont ceux que le modèle ne peut pas toucher.

Simon Willison a condensé la structure en une règle de trois : un agent ayant accès à des données privées, exposé à du contenu non fiable, et disposant d'un moyen de communiquer vers l'extérieur peut être amené à exfiltrer — sans aucun bug logiciel Simon Willison, 2025. Sa prescription est architecturale : supprimer un pilier. La liste des mesures d'atténuation d'OWASP converge vers le même terrain — contraindre ce que le modèle peut faire, et valider ce qu'il produit avec du code déterministe, car le modèle ne peut pas être chargé de se surveiller lui-même.

Lus ensemble, ces éléments disent quelque chose de plus fort qu'« ajouter des défenses ». Ils disent : cessez d'arbitrer des chaînes de caractères. Changez l'interface.

Aucune syntaxe à injecter

L'injection SQL n'a pas pris fin parce que les sanitiseurs se sont améliorés. Elle a pris fin parce que les requêtes paramétrées ont supprimé l'endroit où les données pouvaient devenir du code. Le même mouvement est disponible pour les agents : refuser de laisser le modèle produire une syntaxe exécutable.

C'est ainsi qu'est construit SQAI. Le modèle n'écrit jamais de SQL — ni sanitisé, ni relu, aucun. Son unique outil d'exécution accepte une spec typée : une union discriminée, kind: "query" | "computation", version: "1", validée contre un contrat de capacités ancré par hash avant toute exécution. Nommer une capacité hors contrat et le moteur répond unsupported_operation. Aucune chaîne produite par le modèle n'est jamais concaténée, interpolée ou transmise à un interpréteur — donc ce qu'une injection doit s'approprier, la syntaxe produite par le modèle, n'existe pas dans le pipeline. L'argument complet est développé dans why agents shouldn't write SQL.

Le vocabulaire est fermé, et il est en lecture seule : chacune des 4 574 capacités exposées, par construction. Il n'y a aucune capacité d'écriture à nommer, donc il n'existe aucune incantation — aussi habilement glissée dans un ticket de support — qui produise une écriture. La trappe vers le code hôte, getUnsafeRuntime, n'est accessible depuis aucun outil du modèle.

Et parce qu'une sécurité sérieuse intègre le scénario où le modèle est compromis, les couches derrière l'interface le supposent :

  • Une politique que le modèle ne peut pas adresser. Les listes d'autorisation pour les sources, champs et fonctions sont fixées dans votre code au moment de createSQAI() et appliquées en-process, avant l'exécution. Le schéma d'outil ne comporte aucun champ allowed* — la surface de politique n'entre jamais dans la fenêtre de contexte. Les violations retournent des refus typés et non réessayables : policy_denied_source, policy_denied_field, policy_denied_function. Pourquoi cela surpasse les rôles et les flags : read-only that wasn't.
  • Résultats limités au tenant. Les résultats volumineux sont stockés sous un result_id de 16 octets aléatoires. Un mauvais tenant reçoit result_not_found — pas une erreur de permissions confirmant l'existence des données — et les entrées expirent en 15 minutes.
  • Un canal borné vers le modèle. Au plus 25 lignes et 32 000 octets retournent dans la fenêtre de contexte, la troncature étant toujours déclarée. Un prompt injecté ne peut pas forcer l'outil à déverser une table dans la transcription ; le transport ne le permettra pas.
  • Des erreurs qui ne dressent pas de carte. Les erreurs visibles par le modèle passent par sanitizeMessage, qui supprime les chemins absolus du système de fichiers. La reconnaissance obtient un refus propre, pas un listing de répertoire.

Évaluons cela face à la règle de trois. Données privées : toujours présentes — c'est le rôle du système. Contenu non fiable : toujours présent — les modèles lisent ce qu'ils lisent. Mais les piliers qui transforment l'influence en perte sont coupés. Le chemin d'écriture est absent, et le chemin de lecture est en liste blanche, limité au tenant, plafonné — et hashé, de sorte que ce qu'un agent compromis aurait réussi à demander est rejouable pour le post-mortem.

204 excluded 4 778 capabilities in the contract 4 574 exposed to the model — every one read-only 4 564 deterministic within the declared scope 0 write capabilities — nothing to name, nothing to run
L'intégralité du vocabulaire, à l'échelle — un prompt compromis peut choisir des valeurs différentes, jamais un verbe différent (source : contrat de capacités SQAI).

La frontière, redessinée

L'injection de prompt n'est pas résolue, et le bilan suggère qu'elle ne le sera pas de sitôt. LLM01 occupe sa place depuis deux éditions consécutives. Un système en production disposant des défenses de Microsoft a cédé face à un seul email. Les modèles continueront de lire du texte hostile, et une partie continuera d'atteindre sa cible.

Mais influencer le modèle et atteindre votre base de données sont deux échecs distincts, et seul le premier est inévitable. Le ticket Supabase est devenu un incident parce que l'agent détenait service_role et une surface SQL libre — précisément les piliers dont un détournement a besoin. Le même ticket, dirigé vers un vocabulaire fermé, typé et en lecture seule, devient une ligne de log : unsupported_operation.

La parade à l'injection SQL n'a jamais été un filtre plus intelligent. C'était une frontière où les données ne peuvent pas devenir du code. Tracez cette frontière à nouveau — cette fois autour du modèle — et la nouvelle injection SQL connaît la même fin que l'ancienne. Le reste de la surface est documenté sur /security.