Guide de sécurité des agents IA : JSON malveillant, prompt injection, Tool Calling et risques des données structurées

Au 14 septembre 2026 : dès qu’un agent peut appeler des outils et ingérer du JSON externe, la surface d’attaque passe d’une réponse trompeuse à un vrai appel. Carte défensive : JSON malveillant, injection indirecte, outils trop privilégiés, schémas trop lâches.

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.

DimensionChatbotAgent capable d’appeler des outils
Mode de défaillanceUne réponse fausse ou orientéeUn vrai effet de bord : écriture, requête, mutation de données
Entrée non fiableLe message utilisateur courantTexte utilisateur + JSON externe + résultats d’outils + champs page/mail
ExécutantLe modèle n’émet que du texteHost / MCP Server exécute arguments
ContratLe prompt (souple)JSON Schema + validation Host (dur)
Plus petit correctifAjuster le prompt, politique de refusSchema 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 ressembleSi le Host ne valide pasDéfense
Champs en tropUn objet métier avec des clés non déclaréesFusionné dans la config, ou transmis à l’outil suivantadditionalProperties: false ; jeter les clés inconnues
Confusion de typesUn number/array arrive en string ou objectLes branches d’authz se trompent, ou tout un blob devient un argumentFiger type, enum, format
Payload imbriquéUn champ string qui embarque encore du JSON ou un long briefLe texte intérieur entre dans le prompt et se lit comme des instructionsLimites 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_request aux 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: object sans properties, ou additionalProperties laissé à 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 strict du 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 / inputSchema issus de tools/list collés dans le prompt système ;
  • result de tools/call qui 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 :

  1. Écrire d’abord le contrat. Un JSON Schema par outil : required complet, additionalProperties: false, identifiants via pattern / enum.
  2. Revalider sur le Host. Ne croyez pas que « le modèle a déjà suivi le Schema ». ajv ou équivalent ; refuser par défaut.
  3. Projeter ; ne pas faire transiter tel quel. Copier les champs allowlistés dans un DTO interne, puis appeler l’API métier.
  4. Minimiser les outils. Préférer fetch_public_doc à fetch_url ; préférer la lecture seule à l’écriture.
  5. Autoriser comme l’utilisateur, pas comme le modèle. Vérifier session, tenant et ACL de ressource dans l’implémentation de l’outil.
  6. Encadrer les effets de bord sortants. Envoi, virement, suppression, déploiement en production : moteur de politique ou confirmation humaine.
  7. 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.
  8. 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.