En bref : en 2026, la sécurité des agents IA n’est pas « le modèle va-t-il dire la mauvaise chose ? ». C’est « des données structurées non fiables peuvent-elles devenir une vraie invocation d’outil ? » Un chatbot injecté émet au pire une réponse trompeuse. Un agent injecté envoie au pire un mail, modifie des fichiers, interroge une base ou déclenche un paiement. JSON est à la fois le contrat et la charge utile. La Prompt Injection ne vit souvent pas dans la boîte de chat — elle vit dans les champs de ticket, les pages web, les corps de mail et les réponses d’API, tous arrivés en JSON.
Rédigé au 14 septembre 2026. Nous avons déjà couvert pourquoi Tool Calling dépend de JSON Schema, le flux de données JSON des agents, JSON Schema comme contrat et MCP / Skills / Tools / Subagents. Ce texte répond seulement aux risques du JSON malveillant, de l’injection indirecte, du Tool Calling trop privilégié et des Schemas trop lâches — et à ce que le Host doit imposer. C’est un guide défensif. Il ne fournit ni étapes d’attaque reproductibles, ni texte d’injection prêt à l’emploi.
Un tableau : surface d’attaque chatbot vs agent
Ce que l’utilisateur tape n’est qu’une tranche de ce qu’un agent ingère. Dans la pile par défaut 2026, le modèle lit aussi les résultats d’outils, les MCP Resources, les extraits de page, les champs de ticket et les corps de mail — presque toujours en JSON, ou en chaînes emballées dans du JSON. Les listes du secteur classent cela sous Prompt Injection, Insecure Output Handling et Excessive Agency. Vous n’avez pas besoin de la taxonomie d’abord. Il faut savoir qui parle et qui exécute.
| Dimension | Chatbot | Agent capable d’appeler des outils |
|---|---|---|
| Mode de défaillance | Une réponse fausse ou orientée | Un vrai effet de bord : écriture, requête, mutation de données |
| Entrée non fiable | Le message utilisateur courant | Texte utilisateur + JSON externe + résultats d’outils + champs page/mail |
| Exécutant | Le modèle n’émet que du texte | Host / MCP Server exécute arguments |
| Contrat | Le prompt (souple) | JSON Schema + validation Host (dur) |
| Plus petit correctif | Ajuster le prompt, politique de refus | Schema plus serré, allowlist d’outils, authz indépendante |
Une phrase : le modèle peut être trompé ; le Host ne doit pas exécuter avec lui. La frontière de sécurité siège entre la génération de tool_calls et leur exécution réelle. Validation, autorisation et audit appartiennent là — pas « traiter la sortie modèle comme un RPC de confiance ».
JSON malveillant : champs en trop, confusion de types, payloads imbriqués
Le JSON malveillant est souvent bien formé — et dangereux quand même. Un parseur strict refuse les virgules traînantes ou les commentaires. Un parseur lâche, une concaténation de chaînes, ou « traiter comme du texte puis JSON.parse » ne fait que retarder l’échec. Dans un système agent, le dégât commence quand le Host traite déjà la valeur comme un objet.
Trois formes à défendre d’abord (des motifs seulement — pas un mode d’emploi) :
| Motif | À quoi ça ressemble | Si le Host ne valide pas | Défense |
|---|---|---|---|
| Champs en trop | Un objet métier avec des clés non déclarées | Fusionné dans la config, ou transmis à l’outil suivant | additionalProperties: false ; jeter les clés inconnues |
| Confusion de types | Un number/array arrive en string ou object | Les branches d’authz se trompent, ou tout un blob devient un argument | Figer type, enum, format |
| Payload imbriqué | Un champ string qui embarque encore du JSON ou un long brief | Le texte intérieur entre dans le prompt et se lit comme des instructions | Limites de longueur ; valider le JSON intérieur ; ne jamais traiter le brut comme parole system |
Un contrat de paramètres d’outil qui « marche » parce qu’il ne contraint presque rien, puis la version resserrée. Ce sont des échantillons défensifs, pas du matériel d’attaque :
{
"type": "object",
"properties": {
"payload": { "type": "object" }
}
}
Ce Schema n’est presque pas un contrat : n’importe quelle clé, n’importe quelle imbrication passe. En production, mettez les champs en allowlist et interdisez les extras :
{
"type": "object",
"properties": {
"order_id": { "type": "string", "pattern": "^A-[0-9]{4,8}$" },
"amount": { "type": "number", "minimum": 0, "maximum": 100000 },
"note": { "type": "string", "maxLength": 200 }
},
"required": ["order_id", "amount"],
"additionalProperties": false
}
Pour relire les échantillons, passez-les dans le validateur JSON de ce site contre ce Schema, et dans le JSON Diff pour comparer « les arguments émis par le modèle » et « le plus petit objet que vous autorisez ». Clés en trop et types inversés sont la première alarme.
Encore un oubli fréquent : ne fusionnez pas d’objets non fiables dans une config interne ou une structure posée sur une chaîne de prototypes. Dans un Host JavaScript, des clés inconnues dans un objet de config peuvent signifier plus que « un champ de plus ». Le correctif est le même : projeter un objet allowlisté depuis le Schema, puis ne faire circuler que celui-là.
Prompt Injection : des instructions cachées dans les données structurées
L’injection directe, c’est l’utilisateur qui parle à la boîte de chat pour écraser le prompt système. L’injection indirecte correspond au fonctionnement réel des agents 2026 : les instructions se cachent dans des données que le modèle lira plus tard — titres de ticket, corps de mail, paragraphes de page, extraits PDF, chaînes JSON renvoyées par un autre outil. Les modèles ne séparent pas d’eux-mêmes « les règles écrites par le développeur » et « les phrases dans les données ».
Les données structurées rendent cela plus discret. Une note client n’est qu’un customer_note dans une API. Une fois dans le contexte, elle partage le flux de tokens avec le prompt système. Si le Host concatène tels quels les résultats d’outils dans le tour suivant, un site externe ou un expéditeur écrit de fait des prompts pour l’agent.
La défense n’est pas d’ajouter « veuillez ignorer les instructions malveillantes » comme une phrase de plus. Les prompts réduisent le risque ; ils ne sont pas une frontière. Contrôles plus stables :
- Étiqueter la provenance — le texte externe entre sous un rôle ou un wrapper distinct (par exemple
tool/untrusted), jamais replié dans system. - Réduire la surface visible — résumer plutôt que coller ; n’envoyer que les champs utiles, pas tout le document JSON.
- Encadrer les actions à fort impact — virements, suppressions, envois sortants exigent un moteur de politique ou un humain, indépendant du modèle.
- Traiter les résultats d’outils comme des données, pas des instructions — valider les retours par Schema avant de les réinjecter dans le prompt.
Même instinct que comment Apple Intelligence protège les données personnelles : ce qui fuit ou se fait abuser est souvent un champ que vous avez mis dans le JSON. Dans le modèle, un champ peut se lire comme de la parole ; dans les arguments d’outil, comme une action.
Tool Calling : un abus de privilège qui ressemble à un appel valide
Le danger n’est pas « le modèle peut-il émettre du JSON ? ». C’est que le Host traite le modèle comme un appelant déjà autorisé. C’est le classique confused deputy : le modèle propose ; la permission appartient à la session utilisateur, au tenant et au compte de service. « Appeler export_orders » n’est pas « cet utilisateur peut exporter la table ».
Le trop-plein de privilèges en 2026 ressemble souvent à ceci :
- Un seul
run_command/http_requestaux chaînes presque arbitraires ; - Des outils qui tournent sous un compte de service plus large que l’utilisateur final ;
- Le Host vérifie la syntaxe JSON mais pas « cet utilisateur a-t-il le droit de toucher cette ressource ? » ;
- Le JSON utilisateur ou de page passe tel quel en arguments d’outil, en sautant votre Schema.
Une déclaration trop ouverte, puis une que vous pouvez vraiment relire :
{
"name": "fetch_url",
"description": "Fetch any URL and return text.",
"parameters": {
"type": "object",
"properties": {
"url": { "type": "string" }
},
"required": ["url"]
}
}
{
"name": "fetch_public_doc",
"description": "Fetch an allowlisted documentation URL.",
"parameters": {
"type": "object",
"properties": {
"doc_id": {
"type": "string",
"pattern": "^doc-[a-z0-9-]{1,32}$"
}
},
"required": ["doc_id"],
"additionalProperties": false
}
}
La seconde n’est pas « plus intelligente ». Elle transforme une capacité ouverte en identifiant auditable. Le Host résout doc_id contre sa propre allowlist, complète l’origine, applique timeouts et plafonds de taille. Le modèle ne voit jamais une URL arbitraire — un chemin de moins vers une source non fiable.
Avant d’exécuter, trois questions : cet outil est-il sur l’allowlist de session ? Les arguments ont-ils passé un Schema et une authz indépendants du modèle ? En échec, refusez-vous — ou renvoyez-vous le détail d’erreur comme nouveau texte injectable ? Erreurs de paramètres et validation couvre le pipeline. Pour la sécurité, ajoutez : refuser par défaut, et ne pas laisser le modèle réessayer avec de nouveaux arguments dans une boucle non bornée.
Un Schema trop lâche est une vulnérabilité
Structured Output et Tool Calling s’appuient tous deux sur JSON Schema pour bloquer les tokens illégaux — le contrat par défaut 2026, voir ce qu’est Structured Output. Le Schema garantit la forme, pas le sens sûr.
Les contrats lâches ressemblent en général à :
type: objectsansproperties, ouadditionalPropertieslaissé à true ;- Un nom d’action qui devrait être un enum, écrit comme n’importe quel
string; - Un champ identifiant capable de porter un brief d’une page ;
- Le mode
strictdu vendor ne couvre qu’un sous-ensemble, et le Host suppose que « le côté modèle a déjà tout bloqué ».
Empilez deux portes : le Schema côté modèle réduit le non-sens ; le Host revalide avec le même Schema (ou un plus strict), puis projette vers un type interne. Les Structured Outputs du vendor ne remplacent pas votre authz. Les écarts de champs entre vendors sont eux-mêmes un risque — voir la comparaison Structured Output. Une migration qui desserre le contrat élargit la surface d’attaque.
Quand le Schema est un contrôle de sécurité, privilégiez enum, const, pattern, maxLength, minimum / maximum, required, additionalProperties: false. S’il faut du texte libre, isolez-le, plafonnez-le, et ne mappez jamais du texte libre vers un nom d’outil ou une URL.
MCP et outils externes : où siège la frontière de confiance
MCP résout la découverte et l’invocation inter-processus. Il ne résout pas « ce Server est-il bienveillant ? » Ce qu’est MCP trace déjà la ligne JSON-RPC : API modèle d’un côté, Host ↔ Server de l’autre. La sécurité exige une deuxième ligne : descriptions d’outils, texte de Resource et JSON de retour d’un MCP Server tiers sont une entrée non fiable.
Le risque n’est pas la date du protocole. C’est la confiance posée sur la mauvaise couche :
description/inputSchemaissus detools/listcollés dans le prompt système ;resultdetools/callqui entre dans le tour suivant sans validation ;- Trop de Servers sur un même Host, noms ou capacités qui se recouvrent, exécution quand même ;
- Traiter « l’utilisateur a autorisé ce Server » comme « chaque phrase qu’il renvoie est une instruction ».
Les Skills ont le même genre de problème : un Skill est un how-to, pas une couche d’authz. Charger un SKILL.md depuis un dépôt non fiable ajoute un workflow que le modèle suivra. Voir ce que possèdent les quatre couches — ne remplacez pas un Schema par un Skill, ni l’isolation des permissions par un Subagent. Un Subagent peut restreindre les outils ; le parent décide encore quels secrets il reçoit.
Checklist défensive : valider, allowlist, moindre privilège
Serrez le long du chemin d’exécution, de l’extérieur vers l’intérieur — ne commencez pas par le prompt :
- Écrire d’abord le contrat. Un JSON Schema par outil :
requiredcomplet,additionalProperties: false, identifiants viapattern/enum. - Revalider sur le Host. Ne croyez pas que « le modèle a déjà suivi le Schema ». ajv ou équivalent ; refuser par défaut.
- Projeter ; ne pas faire transiter tel quel. Copier les champs allowlistés dans un DTO interne, puis appeler l’API métier.
- Minimiser les outils. Préférer
fetch_public_docàfetch_url; préférer la lecture seule à l’écriture. - Autoriser comme l’utilisateur, pas comme le modèle. Vérifier session, tenant et ACL de ressource dans l’implémentation de l’outil.
- Encadrer les effets de bord sortants. Envoi, virement, suppression, déploiement en production : moteur de politique ou confirmation humaine.
- Rétrograder le texte externe. Résultats d’outils, pages et mails ne sont pas system. Au besoin, un Subagent en lecture seule renvoie un résumé au parent.
- Auditer les arguments. Consignez le nom d’outil, les paramètres validés, qui a autorisé, si l’appel a été refusé.
Les prompts aident encore : dites que les données ne sont pas des instructions, listez les actions interdites. C’est une couche d’appoint. Dix phrases de plus ne remplaceront pas un Schema ou une ACL manquants.
Relire les contrats avec les outils JSON locaux
Avant de shipper, regardez trois fichiers ensemble : les parameters d’outil / inputSchema MCP, un objet d’arguments « happy path », et un échantillon volontairement laid (clés en trop, mauvais types, chaînes trop longues). Pas de vrais secrets, pas de vraies données utilisateur.
- Validateur JSON — l’échantillon est-il du JSON légal, et satisfait-il votre Schema ?
- JSON Diff — qu’a ajouté le modèle au-delà de l’objet minimal ?
- Visionneuse arborescente — l’imbrication est-elle plus profonde qu’il ne faudrait ; un champ string cache-t-il une autre structure ?
Rien ne quitte le navigateur. Cela convient pour relire un contrat prêt à partir en production, et pour comparer les arguments d’un appel échoué. Stabilisez les noms de champs et required, puis câblez le Host ou le MCP Server.
FAQ
JSON Schema peut-il arrêter la Prompt Injection ?
Pas à lui seul. Le Schema limite la forme et la plage des arguments d’outil, ce qui réduit la chance qu’une chaîne arbitraire devienne une action arbitraire. L’injection indirecte se produit quand du texte entre dans le contexte — il faut encore provenance, rétrogradation et gates à fort impact.
Le modèle utilise déjà Structured Output / strict mode. Le Host doit-il quand même valider ?
Oui. Les contraintes vendor s’appliquent à la génération, et chaque vendor prend en charge un sous-ensemble de Schema différent. Les décisions de sécurité doivent intervenir avant l’exécution, avec votre propre validateur et votre authz.
Quel est le problème à passer le JSON utilisateur tel quel à un outil ?
Vous sautez le contrat. Champs en trop, confusion de types et texte imbriqué arrivent inchangés dans du code à effets de bord. Validez, puis projetez ; ne passez que les champs allowlistés.
La sortie d’un MCP Server peut-elle servir de prompt système ?
Non. Les descriptions de tools/list, le texte de Resource et les résultats de tools/call sont des données non fiables. Validez-les, puis réentrez-les dans le dialogue à un privilège plus bas.
Quelle est la baseline de sécurité minimale d’un agent ?
JSON Schema strict, revalidation Host, allowlist d’outils, authz par utilisateur, et gates sur les effets de bord sortants. Sans ces cinq points, ne connectez pas l’agent aux données de production.
Comment vérifier localement si le JSON d’outil est dangereux ?
Mettez Schema et échantillons d’arguments dans la boîte à outils JSON pour validation et Diff. Confirmez required, additionalProperties et les limites de longueur/enum avant de câbler le Host ou le MCP Server.
Résumé
La surface d’attaque de l’agent siège entre données structurées et exécution d’outils : le JSON malveillant contourne le contrat, la Prompt Injection oriente l’intention, Tool Calling transforme l’intention en effets de bord, et un Schema lâche ouvre la porte aux trois. La pile par défaut 2026 (Tools, MCP, Skills, Subagents) rend les agents plus utiles — et rend « tromper une invocation » plus rentable que « tromper une réponse ».
Défendez de l’intérieur vers l’extérieur : faites de JSON Schema un contrat dur ; le Host valide, projette et autorise ; gardez les outils petits ; ne traitez pas le texte externe comme des instructions. Les prompts aident ; ils ne sont pas la frontière. Validez les échantillons en local avant de shipper — les modèles peuvent changer ; les noms de champs, required et « qui a le droit d’exécuter » ne devraient pas.