Guide · AI agents with tools · Sources read on 29 October 2026
Indirect prompt injection, from CVE to test plan.
Short answer. Indirect prompt injection is an instruction hidden in content an agent reads on its own: a web page, an email, a document, a ticket, a tool result. The model cannot reliably tell data from instructions, so the attacker steers the agent without ever talking to it. Published CVEs show where it ends: data disclosure from Microsoft 365 Copilot (CVE-2025-32711, CVSS 9.3) and code execution through Cursor (CVE-2025-54135). Filters lower the success rate without reaching zero. What holds is architecture: keep private data, untrusted content and an exit channel out of the same agent context, then test every input channel before go-live.
What is indirect prompt injection?
OWASP's LLM01:2025 separates two cases. A direct injection comes from the user's own prompt. An indirect injection happens when the model accepts input from external sources such as websites or files, and that input carries instructions. For an agent, external sources include everything a tool returns: search results, inbox contents, retrieved documents, issue comments, API responses and MCP tool results.
The OWASP Top 10 for Agentic Applications 2026 lists the consequence as ASI01 Agent Goal Hijack: the agent keeps working, but toward the attacker's goal. Two OWASP scenarios show the usual shapes. A page to summarize hides instructions that make the model insert an image whose URL carries the conversation out. A document edited in a RAG store skews the answer of every user whose query retrieves it.
What does indirect prompt injection look like in real CVEs?
| CVE | Product | What NVD records |
|---|---|---|
| CVE-2025-32711 | Microsoft 365 Copilot | AI command injection that lets an unauthorized attacker disclose information over a network. CVSS 9.3 (NVD lists 9.3 and 7.5), published 11 June 2025. |
| CVE-2025-53773 | GitHub Copilot and Visual Studio | Command injection that lets an unauthorized attacker execute code locally. CVSS 7.8, published 12 August 2025. |
| CVE-2025-54135 | Cursor before 1.3.9 | The agent could create a new dotfile without approval. An indirect prompt injection wrote .cursor/mcp.json, which ran a command on the victim's machine: remote code execution without user approval. |
In each case the injection was the way in, and the damage came from what the agent could do next: send data out, run a command, write a configuration file that another component trusted. The Cursor case is the clearest. Creating a file looked harmless, but the file was an MCP configuration, and the next start ran it. The impact is set by the tools and permissions around the model, so most of the defensive work happens there.
Why do filters and system prompts fall short?
The model receives one stream of tokens. Instructions from the developer, the user's request and the text of a fetched page arrive in the same channel, and no marker in that stream is enforced by the model. Prompt-level defenses try to make the boundary visible, and they help:
- Spotlighting. Microsoft researchers marked untrusted text by delimiting it, interleaving a marker token between words (datamarking) or encoding it in base64. Attack success fell from above 50% to below 2% in their GPT-family experiments; datamarking alone took one summarization test from about 50% to 3.1%.
- Benchmarks. AgentDojo's authors built 97 realistic agent tasks and 629 security test cases. They report that existing attacks break some security properties but not all, and that current models already fail many tasks with no attack at all.
- The cost of guarantees. CaMeL (Debenedetti et al., 2025, code published by Google Research) separates the control flow of the trusted query from untrusted data and enforces policies with capabilities at each tool call. On AgentDojo it solves 77% of tasks with provable security, against 84% for the undefended system.
A defense that stops 98% of attempts still lets the rest through, and an agent that reads thousands of documents a week meets many attempts. That is why the next two sections deal with what the agent can reach, not with how well the model resists.
What is the lethal trifecta?
Simon Willison named it on 16 June 2025: an agent that combines access to private data, exposure to untrusted content and the ability to communicate externally can be made to send that data to an attacker. His advice is to avoid the combination entirely. For a review, the useful question is which leg you can remove for each agent:
| Leg | In a real agent | How to cut it |
|---|---|---|
| Access to private data | Mailbox, files, CRM, code, tokens in the environment | Give this agent only the data the task needs; split read-private and read-public agents. |
| Exposure to untrusted content | Web pages, inbound email, tickets, issues, uploaded files, tool and MCP results | Process untrusted content in a context with no tools, or with read-only tools. |
| Ability to communicate externally | HTTP tools, email send, rendered images and links, webhooks, commits and comments | Egress allowlist, no remote images or links in output, human approval on every send. |
The third leg is the one teams miss. A Markdown image in the agent's answer is an outbound request when the client renders it. So are a link the user clicks, a comment posted on a public issue and a commit pushed to a public branch. Count every one of them as an exit.
Which design patterns resist indirect prompt injection?
A 2025 paper by Beurer-Kellner and 13 co-authors describes six patterns that give provable resistance by limiting what untrusted data can influence. Each trades some flexibility for that guarantee.
| Pattern | How it works | Fits |
|---|---|---|
| Action-Selector | The model only picks one of a fixed set of actions. Tool results never come back to it. | Routing and simple commands. Strongest, least flexible. |
| Plan-Then-Execute | The model commits to a plan before it reads untrusted data, so that data cannot add or change actions. | Workflows with a known shape. Arguments can still be steered. |
| LLM Map-Reduce | Isolated sub-agents each process one untrusted item; a reduce step combines their constrained outputs. | Triage of many documents, emails or reviews. |
| Dual LLM | A privileged model plans and calls tools without ever seeing untrusted data; a quarantined model with no tools reads it. | Assistants that must read untrusted content and act. |
| Code-Then-Execute | The model writes a program that calls tools and hands untrusted text to unprivileged models; the program runs. | Complex tasks. CaMeL implements this idea with capabilities. |
| Context-Minimization | Content that is no longer needed, such as the user's prompt after the first step, is removed from the context. | Answers that should not be shaped by earlier input. |
Plan-Then-Execute is the easiest to retrofit into an existing agent, and the most often misread. It fixes which tools run, not what goes into them: a poisoned email can still choose the recipient of a send that the plan allowed. Pair it with argument checks on every tool that leaves the system. CaMeL's code is public in the google-research/camel-prompt-injection repository.
How do you test an agent for indirect prompt injection?
- List the channels. Every tool that returns text written by someone other than the user: search, fetch, inbox, file store, retrieval, ticketing, code host, MCP servers. Each one is a test target.
- Pick goals you can detect. A marker string that appears nowhere else, a call to a tool the task does not need, a request to a URL you control. A goal you can only judge by reading the answer is too weak to count.
- Plant one payload per channel. Place it where real content sits: an email body, a document footer, a ticket comment, a field of a tool result. Vary the wording; one phrasing tells you little.
- Run each case several times. Model output varies between runs, so a single pass proves nothing. Record the attack success rate per channel and the task success rate, since a defense that blocks the task too is not a fix.
- Read the traces, not the answer. The pass condition is zero forbidden tool calls and zero requests to your URL, taken from the agent's logs. A clean final answer can hide a call that already happened.
- Repeat after each change of model, system prompt, tool or data source.
Which open-source tools help?
| Tool | What it does | Use it for |
|---|---|---|
| AgentDojo | Agent benchmark: 97 realistic tasks and 629 security test cases across tool-using suites, with pluggable attacks and defenses. | Compare defenses and models on an agent with tools; adapt a suite to your own tools. |
| garak (NVIDIA) | LLM vulnerability scanner. Its latentinjection probes bury instructions inside reports, resumes, legal and fact snippets, translations and whois records. | Model-level baseline before you build the agent, and after each model change. |
| PyRIT (Microsoft) | Red-teaming framework. Its XPIA workflow plants attack content in one place (a file, a mailbox, a store), then triggers the processing target and scores the response. | End-to-end tests on your own agent, with your real channels. |
AgentDojobenchmark runpython -m agentdojo.scripts.benchmark -s workspace --model gpt-4o-2024-05-13 --defense tool_filter --attack tool_knowledge
garaklatent injection probespython3 -m garak --target_type openai --target_name gpt-5-nano --spec probes.latentinjection
The three cover different layers. garak measures how a model reacts to instructions buried in text, before any tool is involved. AgentDojo measures an agent with tools in a controlled environment, which makes it the place to compare defenses. PyRIT's cross-domain prompt injection (XPIA) workflow drives your own system: one target plants the attack content where your agent will find it, a callback makes the agent process it, and a scorer decides whether the attack succeeded. Only the last kind of test tells you about your real channels.
What should be true before go-live?
- Every untrusted input channel is listed, and each has at least one planted test case with a recorded result.
- No agent context holds all three legs of the trifecta, or the missing control is written down with an owner.
- Tools that send, write, pay or delete require approval that shows the real arguments, not a summary.
- Outbound traffic goes through an allowlist, and the client does not render remote images from model output.
- Logs record which source each piece of context came from, every tool call with its arguments, and every outbound destination, so an incident can be traced back to the document that caused it.
- The tests run again on each model, prompt or tool change.
