The request was as harmless as they come: summarise the tickets opened this week. It came from an operations analyst, with his own account, during business hours, inside an agent the security team had approved. The agent did exactly what it was asked — it read the tickets. And in one of them, in the middle of a perfectly legitimate complaint, there was a paragraph that was not meant for the analyst to read: it was meant for the agent. Nobody typed a malicious prompt. The user prompt was clean. The hostile instruction came in through the door nobody was watching, which is the door the content comes back through.
The service entrance The security review of nearly every AI project I see in the field aims at the same spot: what the user types. Input filter, jailbreak classifier, banned term list, a red team writing creative prompts. All of it is necessary, and none of those controls looks at where the expensive attack actually happens — the content the tool brings back into the context window, without anyone typing a single line.
What is indirect prompt injection?
It is when the text that manipulates the model does not come from the person talking to it, but from content the agent itself went and fetched. The Microsoft Security Response Center draws the line in one sentence: “Unlike direct prompt injection, where the attacker is the user of the LLM, indirect prompt injection involves the attacker injecting instructions into the interaction between a victim user and the LLM.” In direct injection, the attacker is the user. In indirect injection, the attacker is a third party who wrote earlier, somewhere else, expecting an agent to pass by.
The 2026 edition of the OWASP Top 10 for LLM Applications treats this as a named subtype and lists the delivery surfaces with a precision worth quoting: “The model ingests content from an external source (a web page, a document, an email, a tool response, a retrieved RAG passage, an image, an MCP server’s output, a database row, or an issue title) that contains data which acts as prompt injection. The user did not supply or see those instructions.”
Notice what is on that list: a tool response and an MCP server’s output. It is no longer just “websites and files”, as in the previous edition. Those are the return channels of your own agent design.
The root cause is architectural, and OWASP states it without hedging: “LLMs make no architectural distinction between ‘instructions’ and ‘data’ (both are tokens on the same stream), so there is no clean equivalent to parameterized queries.” That is the most important sentence in this article. In databases we solved SQL injection with prepared statements: the command travels on one channel and the data on another, and the engine never confuses the two. In an LLM that separate channel does not exist. Instruction and data arrive as tokens on the same stream, and the separation between them is one of intent, not of protocol.
The problem it solves
The problem is where the perimeter sits. OWASP sums up the effect in nine words: “Defenses that focus only on the chat surface miss this entirely.”
When a design assumes the perimeter is the prompt, three predictable things happen — and I have seen all three in the same quarter:
- The security review approves the agent through the text box. Jailbreak is tested, offensive content is tested, system prompt leakage is tested. None of that touches the path the attack comes in through, which is the search tool, the inbox, the document index.
- The tool catalogue grows without an owner. Every new tool is a new return channel. When the tool is a third-party MCP server, what enters the context is written by someone outside your organisation — and that is supply chain, not integration.
- The privilege belongs to the user, and nobody recalculates the reach. The action runs with the credential of the person who asked. The attacker needs no access at all: he borrows yours. OWASP puts it better than I can: “The attacker does not need to compromise the backend directly. They place text where the developer’s LLM will read it, and the LLM, operating with the developer’s privileges, does the work.”
This article is the step before guardrails and agent evaluation, published here in August. That one was about containment and measurement — how to contain the output and how to prove it improved. This one is about where the hostile instruction comes from, which is the question that decides where the control has to sit.

How it works — step by step
- The attacker writes first. He plants the text where he knows an agent will pass: a support ticket, a public issue, a package README, a page the crawler indexes, an email sent to an address someone asks Copilot to summarise.
- He writes for the machine, not for the person. The passage is usually invisible or irrelevant to a human reader — an HTML comment, white-on-white text, a PDF footer, a metadata field.
- The user makes a harmless request. Summarise, compare, reply to this ticket. The prompt is legitimate and would pass any review.
- The tool fetches the content. Vector retrieval, inbox reading, a call to an MCP server, browsing. The return enters the context window with no human review — that is the step in the design almost nobody marks on the diagram.
- The model does not separate data from instruction. With no distinct channel, the planted paragraph competes on equal footing with the system prompt and with the user request.
- The action runs with the asker’s privilege. If the agent has a mail-sending tool, a repository-writing tool or an API call, the attacker has just operated with the victim’s credential.
- The output carries the data out. A markdown image with an attacker-controlled URL, a clickable link, a request to an external domain. That is where injection becomes exfiltration.
UNTRUSTED CHANNEL TOOL CONTEXT WINDOW ACTION
web page / email ──┐
indexed document ──┼──► tool ──► system prompt send email
issue / README ──┤ invocation + user request ──► write to repo
MCP server ──┘ │ + UNREVIEWED RETURN call an API
│ │ │
[1] classify [2] mark the [3] approve the
what comes back untrusted span consequential action
Those three brackets are the only places a deterministic control can sit. Everything else is probabilistic.
The list moved, and what that tells us
The OWASP GenAI Security Project finalised the 2026 edition of the Top 10 for LLM Applications on 4 August 2026, according to the project’s official repository, and launched it publicly on 2 September 2026, in the press release that also announced the donation of the Agent Control Standard. Two different dates for two different facts; worth not blurring them.
The ordering tells a story:
| Risk | 2025 | 2026 | Reading |
|---|---|---|---|
| Prompt Injection | LLM01 | LLM01 | Still first, but the text now names the indirect subtype and lists tools and MCP as surfaces |
| Excessive Agency | LLM06 | LLM03 | Up three places: the biggest climb of the edition |
| Supply Chain | LLM03 | LLM04 | Swaps position with the one above |
What moved up was not the injection — it was its consequence. Excessive Agency is the vulnerability that turns a manipulated answer into damage, and OWASP’s own text points at the origin: “Common triggers include… direct/indirect prompt injection from a malicious user, an earlier invocation of a malicious/compromised tool, or… a malicious/compromised peer agent.”
The 2026 edition also organises surfaces by trust, and that table belongs in the design of any corporate agent:
| Layer | What it is | OWASP’s own example |
|---|---|---|
| Untrusted | Public content, unknown author | Public web pages, emails from unknown senders, search results |
| Semi-trusted | Identifiable origin, but not under your control | Issue titles in a public bug tracker, package READMEs and changelogs, third-party API responses |
| Trusted | Content under the organisation’s control | The developer’s own repositories, databases, internal documents and mail |
The classic mistake is treating everything that passed authentication as trusted. A ticket opened by an authenticated customer is, at best, semi-trusted: the identity is known, the content is not.
There is also a concept OWASP imports that compresses the risk into three conditions — the lethal trifecta: an agent that can simultaneously access private data, ingest untrusted content and communicate externally has the conditions for high-impact exploitation. You do not have to remove all three. Breaking one is enough.
Where Microsoft puts the intervention point
Here is the detail that struck me most while rereading the documentation for this article, because it confirms the thesis from the product side. Prompt Shields, in Azure AI Content Safety, classifies two attack types — and applies each at different intervention points:
“User prompt attacks are malicious prompts that attempt to bypass system instructions or safety training. In Microsoft Foundry, the system scans for these attacks at the user input intervention point. Document attacks are hidden instructions in third-party content, such as documents, emails, and web pages, that attempt to take control of the model session. In Foundry, the system scans for these attacks at the user input and tool response intervention points.”
Read that last line again. The document attack — which is Microsoft’s name for indirect injection, the XPIA — is also scanned at the tool response. The product already recognises the tool as an entry point. What is usually missing is for the design to recognise it too.
| Attack type | Product name | Intervention point |
|---|---|---|
| Prompt typed by the user | user prompt attack | user input |
| Third-party content (XPIA) | document attack | user input and tool response |
It is a single call with two fields, and the response separates the two analyses:
POST <endpoint>/contentsafety/text:shieldPrompt?api-version=2024-09-01
{
"userPrompt": "summarise the tickets opened this week",
"documents": ["...ticket body, exactly as the tool returned it..."]
}
{
"userPromptAnalysis": { "attackDetected": false },
"documentsAnalysis": [ { "attackDetected": true } ]
}
That documentsAnalysis is the field almost no pilot fills in. The userPrompt is sent and that is that — and the classifier ends up watching the wrong door. A naming artefact helps the confusion: the REST operation is still called detect-text-jailbreak, inherited from the feature’s old name (Jailbreak risk detection), even though the product has been called Prompt Shields since the March 2024 public preview. The API has been generally available since August 2024.
In Foundry, Prompt Shields is part of the guardrails and controls system: you switch it on when configuring guardrail controls for a model deployment or for an agent.
Marking what is not trusted: spotlighting
Classifying is one half. The other is telling the model which span of the context does not deserve trust. That is what Microsoft calls spotlighting, currently in preview:
“Spotlighting tags input documents with special formatting that identifies them as lower-trust content. The service transforms the document content by using base64 encoding, and the model treats it as less trustworthy than direct user and system prompts.”
Three things have to enter the design, and all three are documented:
- It is off by default. “Spotlighting is off by default.” Nobody inherits this protection by accident.
- It costs tokens. Base64 encoding increases the document’s token count, which raises cost and can push a long document past the input limit.
- It only applies to the Chat Completions API. “Spotlighting is available only for models used through the Chat Completions API.”
It is worth knowing where this came from. Microsoft’s original research describes three modes — delimiting, datamarking and encoding — where the first surrounds the content with randomised delimiters and the second inserts a special token throughout the untrusted text. The product ships only the third today. If your design needs explicit delimiting, that part is yours to build.
And here is the point I keep repeating to every customer: none of this is sufficient on its own. Microsoft’s own security pattern page says so in as many words — “Use multiple layers | No single solution is sufficient—combine probabilistic and deterministic defenses.” The MSRC team is even more direct about the design posture: “Microsoft’s approach therefore does not rely on our ability to block all prompt injections. Instead, we design systems such that even if some prompt injections are successful, this will not lead to security impacts for customers.”
That is the sentence that should steer the architecture. The goal is not to block every injection — it is to make a successful injection fail to become impact.
MCP protects the token, not the content
The current revision of the Model Context Protocol is 2026-07-28, and it carries a structural change that still surprises anyone who stopped following: “MCP is stateless and has no protocol-level sessions.” If you memorised the handshake-with-session model, reread the specification.
For the subject of this article, though, the most relevant finding is what the security specification does not contain. I read the current revision’s Security Best Practices document end to end: it covers confused deputy, token passthrough, SSRF, state handle hijacking, local server compromise, authorization URL validation, mix-up attacks and scope minimisation. There is no section called Prompt Injection. The protocol’s threat model is one of authorization and transport — tokens, consent, sessions, redirects. The risk of a server handing hostile text back to the model is left to the client and application layer.
That is not a criticism of the protocol; it is a division of responsibility, and it has to be explicit in your design. What the specification does offer on the content side is three concrete things:
- Invocation is model-controlled. “Tools in MCP are designed to be model-controlled, meaning that the language model can discover and invoke tools automatically based on its contextual understanding and the user’s prompts.”
- The human in the loop is a recommendation, not an obligation. “For trust & safety and security, there SHOULD always be a human in the loop with the ability to deny tool invocations.” It is
SHOULD, notMUST— and that difference becomes an architecture decision on your side. - A tool annotation is not a guarantee. “Clients MUST consider tool annotations to be untrusted unless they come from trusted servers.” The schema is blunter still: the properties of
ToolAnnotationsare hints, and “Clients should never make tool use decisions based onToolAnnotationsreceived from untrusted servers.”
The schema defaults deserve their own paragraph, because they are counter-intuitive in the right direction: readOnlyHint defaults to false and destructiveHint defaults to true. In other words, an unannotated tool is, by specification, presumed not read-only and potentially destructive. The default is pessimistic on purpose. Treating an unannotated tool as harmless contradicts the specification itself.
On the OWASP side, the matching mitigation is explicit and operational: “Pin, sign, and verify every MCP server and third-party tool package, audit tool descriptions for hidden instructions, and monitor tool composition.” Auditing the tool description is the item most often missing from the reviews I sit in — the description also enters the model’s context, and therefore it is also an injection channel.
When this actually happened
Two vendor references, with public identifiers, to take the subject out of hypothetical territory.
CVE-2025-32711, known as EchoLeak. An information disclosure vulnerability in Microsoft 365 Copilot, CVSS 9.3 with a network vector, published on 11 June 2025 and last updated in February 2026, credited to Aim Labs. What makes it the exemplary case for this article is the vector: retrieved content, not typed content. OWASP’s matching scenario describes a crafted email that leads a productivity assistant to exfiltrate organisational data with no user interaction — and notes that the attack bypassed both the prompt-injection classifier and the link-redaction filter.
AWS-2025-019, an AWS security bulletin dated 7 October 2025, which uses the phrase in a title: “AWS Kiro: Arbitrary Code Execution via Indirect Prompt Injection”. The same bulletin lists remote code execution in Amazon Q Developer through prompt injection and secret leakage over DNS. The fix, in every case, has the same name: human-in-the-loop confirmation.
One honesty caveat, because this site has already been burned by second-hand sourcing: the malicious prompt text that circulated in the press about the earlier Amazon Q incident (bulletin AWS-2025-015, July 2025) does not appear in AWS’s own bulletins. It comes from journalism. I do not quote it as if it were a vendor document.
Production best practices
- Inventory the return channels, not just the tools. For every tool, answer this: who writes the content it hands back? If the answer is not “my organisation”, the channel is untrusted by definition.
- Classify at the tool boundary. Fill the Prompt Shields
documentsfield with the tool return, not just theuserPrompt. It is one line of code and it is the difference between watching the right door and the wrong one. - Mark the untrusted content. Switch spotlighting on where it applies, and where it does not, implement explicit delimiting in your orchestrator. Tell the model which span is data and which is instruction, since the channel will not do it for you.
- Require human approval for consequential actions. The Foundry guidance is literal: “Treat tool outputs as untrusted input and validate critical values before acting on them. Validate structured outputs against an expected schema, allow only known fields and operations, and require user approval before consequential actions.”
- Break the trifecta. If the agent reads untrusted content and reaches private data, take away its free outbound channel: restricted egress, allow-listed domains, no rendering of images with arbitrary URLs.
- Treat an MCP server as a software dependency. Pin the version, verify the origin, audit the tool descriptions and re-review on every update. For central governance of MCP tools, Foundry points to AI Gateway, in preview.
- Do not trust annotations from a server you do not control. A
readOnlyHintfrom a third party is a statement of intent, not an access control. - Assume the attack succeeds. Ask of every tool: if the model is tricked into calling this with the worst possible arguments, what is the damage? If the answer is unacceptable, the control is not a better filter — it is removing the tool or putting approval in front of it.
The approval mechanism is concrete and lives in code, not in process. In Agent Framework, a function starts requiring confirmation through one parameter:
@tool(approval_mode="always_require")
def send_email(recipient: str, subject: str, body: str) -> str:
"""Sends email on behalf of the user."""
...
# The run returns ToolApprovalRequestContent instead of a final answer:
# the agent stops, the app shows the proposed call, and a person approves or denies.
In .NET the equivalent is wrapping the function in ApprovalRequiredAIFunction. The documentation calls the design by its name: “When agents require any user input, for example to approve a function call, this is referred to as a human-in-the-loop pattern.”
What has to be in place
The order below is causal, not chronological: each stage exists because the previous one is standing.
Foundation — authority over what the agent reads. There is an inventory of return channels per agent, each channel is classified as trusted, semi-trusted or untrusted, and tool returns are classified before becoming context. The tool catalogue has an owner, and no new tool enters without a review of the channel it opens.
It is in place when you can answer, for any agent in production, who wrote each piece of text that entered the context window of the last answer.
Production with context — privilege stops being automatic. Consequential actions require explicit human approval, tools follow least privilege, and the trifecta of private data access, untrusted content ingestion and external communication is broken at a minimum of one point per agent. Untrusted content reaches the model already marked.
It is in place when a successful injection in the test environment produces an approval request rather than a side effect.
Scale and efficiency — the platform becomes a product. MCP servers and tool packages are treated as dependencies: pinned version, verified origin, audited description. There is telemetry linking tool call, retrieved content and executed action, so that an incident can be reconstructed. Defence in depth is a platform default, not a per-team decision.
It is in place when a new agent is born with boundary classification, approval for consequential actions and restricted egress by inheritance, with nobody having to remember.
Frequently asked questions (FAQ)
Does an injection classifier solve the problem?
No, and that is not what it claims. MSRC itself describes Prompt Shields as a probabilistic, classifier-based approach. EchoLeak is the practical proof: the attack bypassed both the injection classifier and the link filter. A classifier reduces attack volume; it does not replace deterministic control over what the action is allowed to do.
How is this different from what the guardrails article covered?
Guardrails and evaluation are about containment and measurement: containing the output and proving quality improved. This article is about where the hostile instruction comes from — the entry channel. They are complementary layers: without knowing the channel, the guardrail is applied in the wrong place.
Does indirect injection affect an agent that only reads internal data?
It does, with lower probability and identical impact. Internal content is trusted in OWASP’s table, but one public form feeding an internal table, one ticket opened by a customer or one document attached by a supplier is enough for externally authored content to cross the boundary. Classify by the origin of the text, not of the system.
Does MCP make the problem worse?
It widens the surface and shifts the owner. The current specification has no prompt injection section: its threat model is authorization and transport. Adding a third-party server adds a return channel and a tool description that both enter the model’s context — which is why OWASP tells you to pin, sign, verify and audit tool descriptions.
Does human approval kill automation?
It does if you apply it to everything. That is why the rule is by consequence, not by call: reads and queries stay automatic, and approval is reserved for what sends, writes, deletes, transfers or publishes. That is exactly the correction AWS applied in the October 2025 bulletin cases.
Conclusion
Indirect prompt injection is not a model defect to be fixed in the next version. It is the consequence of an architectural property: instruction and data travel on the same channel, and there is no prepared statement for natural language.
While that remains true, the perimeter cannot stay in the text box. It has to sit where the content enters — at the tool boundary —, where the action happens — on privilege — and where the data would leave — on egress. Microsoft already places an intervention point at the tool response; OWASP already names the tool and the MCP server as delivery surfaces; the MCP specification already warns that a third party’s annotation does not deserve trust.
The three documents say the same thing from different angles: the user prompt is the front door, and nobody has come in that way for a long time.
Official references
- OWASP Top 10 for LLM Applications 2026
- Official OWASP GenAI LLM Top 10 repository
- Agent Control Standard
- OWASP Top 10 for Agentic Applications
- Prompt Shields — user prompt and document attack detection
- Prompt Shields quickstart
- Defending against indirect prompt injection
- Tool best practices in Foundry Agent Service
- Tool approval in Agent Framework
- MCP specification — tools
- MCP specification — security best practices
- CVE-2025-32711 — Microsoft 365 Copilot
- AWS security bulletin AWS-2025-019
👉 If you are putting an agent into production — especially in a bank or an insurer, where the tool reads tickets, email and customer documents, start with the inventory of return channels before tuning one more input filter.
Want to talk through indirect injection, tool boundaries and action approval in agents? Reach me on LinkedIn.
