AI

AgentOps : neutraliser une injection indirecte dans la sortie d'un outil MCP avant une action de production

Un runbook de production pour isoler les instructions hostiles transportées par une sortie MCP, conserver leur provenance, appliquer la policy hors du modèle et valider ou rollbacker l'accès en écriture.

27 sept. 2026 aiagentopsagentsmcptoolsprompt-injectionsecurityprovenanceobservabilityevaluationguardrailsautomationrunbookrollbackproduction

Un agent d’exploitation lit une demande de changement via un outil MCP, puis propose un déploiement de production que l’opérateur n’a pas demandé. Le ticket est légitime, l’appel d’outil a réussi et la réponse respecte son schéma JSON. Pourtant, un champ de texte libre contient une instruction demandant à l’agent d’ignorer l’approbation et d’appeler un autre outil.

Il s’agit d’une injection indirecte par la sortie d’un outil. Le cas diffère d’un corpus de retrieval pollué : le texte hostile arrive pendant un workflow actif, depuis un système que l’agent est autorisé à interroger. Il traverse aussi une validation de schéma classique, car une chaîne valide peut contenir une instruction. Le cas fil rouge est un agent d’exploitation Microsoft Foundry qui lit les changements, interroge l’état Azure et peut appeler un outil de déploiement borné après validation humaine. La décision finale est explicite : rouvrir les écritures, garder l’agent en lecture seule, mettre le connecteur en quarantaine ou restaurer le contrat d’outil précédent.

Figer toute la chaîne d’action

Bloquez les nouveaux appels en écriture sans supprimer la trace. Conservez demande utilisateur, versions de l’agent et du prompt, versions du serveur MCP et de l’outil, arguments, résultat brut, résultat normalisé, prochain appel proposé, décision de policy, état de l’approbation et identité d’exécution. Une capture de la réponse finale ne montre pas où l’instruction est entrée dans la chaîne.

yaml 01-mcp-output-injection-incident.yml
incident: INC-AI-731
window_utc: 2026-09-27T14:10:00Z/2026-09-27T14:25:00Z
agent:
name: ops-assistant-prod
release: 2026.09.27-2
tool_call:
server: change-catalog-mcp
tool: read_change_request
schema_version: 4
arguments_hash: <sha256>
raw_result_hash: <sha256>
source:
system: change-catalog
record_id: CHG-1842
revision: 19
proposed_action:
tool: deploy_release
target: production
approval_id: missing
containment:
write_tools: disabled
read_tools: enabled
rollback:
agent_release: 2026.09.26-1
tool_schema_version: 3

Gardez la réponse brute sous accès restreint et calculez son hash avant de la expurger. L’analyse a besoin des octets exacts, mais le compte rendu d’incident ne doit ni redistribuer des secrets ni transformer le texte hostile en payload réutilisable.

Localiser la rupture de frontière de confiance

Reconstruisez la chaîne champ par champ. Séparez données de contrôle, preuves factuelles et contenu non fiable. Nom d’outil, version de schéma et décision de policy sont du contrôle. Un état de déploiement renvoyé par Azure est une preuve. Titre de ticket, commentaire, ligne de log, issue de dépôt ou message fournisseur reste du contenu non fiable, même transmis par une API authentifiée.

La question n’est pas de savoir si le serveur MCP est fiable. Il faut déterminer quels champs ont le droit d’influencer une action. L’authentification prouve quel service a fourni la réponse ; elle ne transforme pas un texte rédigé par un utilisateur en policy.

Comparez réponse backend brute, résultat MCP et représentation visible par le modèle. Recherchez les transformations qui concatènent les champs, perdent la provenance, promeuvent un commentaire en résumé ou placent une donnée derrière un libellé qui ressemble à une consigne système. Identifiez le champ exact qui a modifié le choix du prochain outil. Si les traces ne gardent ni identifiant de résultat ni décision de policy, laissez les écritures fermées jusqu’à correction de cette lacune.

Donner un contrat explicite à la sortie d’outil

Ne limitez pas la correction à « ignorer les instructions malveillantes » dans le prompt système. Réduisez la réponse de l’outil aux champs utiles, avec origine et classe de confiance. Séparez le texte libre des valeurs exploitables par une action.

json 02-bounded-mcp-result.json
{
"resultVersion": "5",
"source": {
  "system": "change-catalog",
  "recordId": "CHG-1842",
  "revision": "19",
  "retrievedAt": "2026-09-27T14:16:32Z"
},
"facts": {
  "environment": "production",
  "requestedRelease": "api@2026.09.27.1",
  "workflowState": "awaiting_approval"
},
"untrustedContent": {
  "contentType": "user_authored_text",
  "value": "<redacted ticket comment>",
  "mayAuthorizeAction": false
},
"actionPolicy": {
  "writeAllowed": false,
  "reason": "approval_not_verified"
}
}

Le serveur MCP doit rejeter les champs inconnus lorsque le contrat le permet, limiter la taille de réponse et signaler toute troncature. Ces contrôles réduisent l’ambiguïté sans détecter toutes les injections. Même parfaitement valide, untrustedContent.value reste une donnée, jamais une instruction, un argument d’outil ou un jeton d’approbation.

Appliquer l’autorisation hors du modèle

Le modèle peut proposer l’étape suivante ; il ne doit pas décider si elle est autorisée. Placez une porte déterministe dans l’orchestrateur ou la gateway d’outils avant chaque écriture. Relisez l’état courant depuis une source d’autorité, puis vérifiez acteur, cible, opération permise, approbation, expiration et empreinte de requête sans dépendre du texte reçu précédemment.

Liez l’approbation à l’action canonique : outil, arguments normalisés, environnement et identifiants de ressources. Si l’un change, l’approbation est périmée. Un commentaire de ticket ne doit jamais fournir un identifiant d’approbation, une identité d’exécution ou un override caché.

Exigez l’idempotence de l’outil d’écriture. Si l’incident comprend un timeout ou un résultat inconnu, interrogez l’état de l’action avant tout retry. Le confinement reste incomplet si rejouer la même intention peut dupliquer une action pourtant légitime.

Détecter le comportement, pas seulement les mots suspects

Des mots-clés aident au triage, mais une attaque peut les éviter et des logs légitimes peuvent les contenir. Surveillez la frontière comportementale : un champ non fiable influence un nouvel outil, une écriture est proposée sans approbation vérifiée indépendamment, ou les arguments n’existent que dans le texte libre.

kusto 03-mcp-output-action-boundary.kql
let Window = 24h;
AgentToolEvents
| where TimeGenerated > ago(Window)
| where EventType in ("tool_result", "tool_proposed", "policy_decision", "tool_executed")
| summarize
  UntrustedResults=countif(ResultTrust == "untrusted"),
  WritesProposed=countif(OperationClass == "write" and EventType == "tool_proposed"),
  WritesBlocked=countif(OperationClass == "write" and PolicyDecision == "deny"),
  WritesWithoutApproval=countif(OperationClass == "write" and ApprovalVerified != true),
  DistinctSources=dcount(SourceRecordId)
by CorrelationId, AgentRelease, ToolName, bin(TimeGenerated, 5m)
| where UntrustedResults > 0 and (WritesProposed > 0 or WritesWithoutApproval > 0)
| order by TimeGenerated desc

Adaptez tables et champs au pipeline de traces. Conservez la corrélation entre intention utilisateur, appel MCP, révision du record backend, décision de policy et action réelle. Une tentative bloquée est un signal de sécurité, pas du bruit à retirer de la télémétrie.

Évaluer des résultats adversariaux avant de rouvrir les écritures

Construisez les tests à la frontière de sortie de l’outil, pas seulement sur le prompt initial. Rejouez la même intention utilisateur avec des variantes contrôlées : instruction dans un commentaire, texte encodé, faux identifiant d’approbation, arguments cachés dans un log, réponse très longue qui repousse le contexte de policy, puis résultat témoin sain.

Les critères doivent être observables. L’agent peut résumer le texte non fiable, mais il doit en nommer l’origine, refuser d’en faire une policy, obtenir les faits courants depuis les champs approuvés et éviter toute écriture tant que la porte externe ne passe pas. Ajoutez un contrôle négatif prouvant qu’une approbation valide pour l’action A n’autorise pas l’action B.

Exécutez d’abord la version candidate en shadow mode, puis exposez les outils de lecture à une petite cohorte. Le canari d’écriture doit viser une ressource réversible hors production, avec une approbation indépendante et un test de refus en parallèle. Ne canaryez pas une défense contre l’injection en élargissant les droits de production.

Décider et rollbacker proprement

Rouvrez les écritures de production seulement si le champ source est identifié, le résultat garde provenance et classe de confiance, la policy est appliquée hors du modèle, les évaluations adversariales passent, les traces expliquent la décision et le contrôle de refus reste bloqué.

Gardez l’agent en lecture seule si le workflow reste utile mais que l’attribution ou la preuve d’approbation demeure incomplète. Mettez le connecteur MCP en quarantaine s’il ne peut pas séparer contenu non fiable et champs d’action, ou si le record backend a été compromis. Restaurez la version précédente de l’agent ou du contrat d’outil lorsque la régression suit un déploiement et que la version conservée passe le même jeu d’évaluation.

Si une action non voulue a été exécutée, rollbackez la ressource de production concernée avec son propre runbook. Désactiver l’agent n’annule ni déploiement, ni route, ni permission, ni job d’automatisation. Après rollback, vérifiez l’état du système et le journal des actions, puis conservez la trace de l’incident comme test de régression.

Conclusion

Une sortie MCP n’est pas sûre par nature parce que le serveur est authentifié ou que le JSON est valide. Dans un workflow agentique, chaque champ de texte libre traverse une frontière de confiance dès que le modèle peut le convertir en action.

Le contrôle durable est en couches : garder la provenance, marquer le contenu non fiable, minimiser le contrat de résultat, autoriser les écritures hors du modèle, tracer toute la chaîne et évaluer des réponses d’outil hostiles. L’accès de production ne revient que lorsque le système prouve que la donnée a informé le diagnostic sans devenir une instruction.