Moayyad Faris
Security Engineering12 min read· Published 2026-07-31

Defending Autonomous AI Agents Against Indirect Prompt Injection

It's the same confused-deputy shape as the SSRF bug this site's own tools defend against, wearing a different payload — and the design patterns that actually hold up against it.

Moayyad Faris
VP of Engineering & Software Architect

Written from having shipped the SSRF-guard stack behind this site's own Security Header Analyzer and RTL Website Checker — DNS pinning, resolved-IP validation, per-redirect-hop re-validation — which defends against the exact same confused-deputy failure shape this post covers, just with a URL as the untrusted input instead of a document.

Indirect prompt injection is the confused-deputy problem this site's own SSRF-guard code already defends against, wearing a different payload: untrusted content tricks a privileged system into acting on an attacker's behalf. This post covers OWASP's direct-vs-indirect distinction, Simon Willison's lethal-trifecta model, and which agent design patterns — dual-LLM, CaMeL, plan-then-execute — actually hold up, with their real tradeoffs.

1. The Same Confused-Deputy Shape, a Different Payload

The confused deputy problem (CWE-441) is a decades-old security concept: a privileged program is tricked by a less-privileged party into misusing its own authority on that party's behalf, because the program can't tell "this instruction came from someone I trust" apart from "this instruction is embedded in data I was just asked to process." Server-Side Request Forgery is one concrete instance — a server-side feature accepts a URL from an untrusted party and fetches it with the server's own privileges, so the server ends up making a request on the attacker's behalf that the attacker couldn't have made directly. This site's own SSRF-guard — DNS pinning, resolved-IP validation against private ranges, per-redirect-hop re-validation — exists because a URL is untrusted input, full stop, regardless of how legitimate the request that produced it looked.

Indirect prompt injection is the same shape, with the untrusted input swapped from a URL to a document. OWASP's LLM01:2025 entry defines it plainly: "An LLM accepts input from external sources, such as websites or files," and embedded content alters model behavior when the model interprets it. A support agent that reads incoming emails, a coding agent that reads a repository's README, a research agent that summarizes a webpage — each one is a confused deputy the moment the content it processes can also issue it instructions. The vulnerability doesn't require the content to look suspicious to a human: OWASP is explicit that "inputs affecting models need not be perceivable to humans, only parseable by the system," which is exactly why an injection hidden in white-on-white text, HTML comments, or a PDF's metadata works as well as one written in plain sight.

The reason this parallel matters beyond being a tidy analogy: the defense-in-depth reasoning that produced DNS pinning and redirect re-validation — never trust the first answer, re-verify at every hop where untrusted input could have changed something — is the same reasoning that motivates every serious prompt-injection defense pattern below. Neither problem is solved by a single check at the front door.

2. The Lethal Trifecta: When an Agent Becomes Exploitable

Simon Willison's framing of when an agent is actually at risk is the sharpest available: an agent is exploitable when it combines three capabilities at once — access to private data, exposure to untrusted content, and the ability to communicate externally. In his words, "if your agent combines these three features, an attacker can easily trick it into accessing your private data and sending it to that attacker."

Each capability alone is fine. A read-only agent with no external communication can be injected all day and never leak anything, because it has nothing to exfiltrate through. An agent with private-data access but no exposure to untrusted content has no injection vector to begin with. It's the combination that's lethal, because the underlying mechanism is that an LLM cannot reliably separate instructions from data once both arrive as the same sequence of tokens — Willison's point that everything gets "glued together into a sequence of tokens" is why role labels in a prompt are a convention the model is trying to follow, not a security boundary it's structurally incapable of crossing.

This gives a concrete design question for any agent before it ships: which of the three legs does it actually need? An agent that summarizes documents doesn't need external communication. An agent that drafts emails for human review doesn't need to send them itself. Removing one leg of the trifecta is a smaller, more reliable change than trying to make the model immune to injected instructions.

3. Why Direct vs. Indirect Changes the Fix

OWASP's LLM01:2025 catalog lists nine real attack scenarios, and the pattern across them is that trust in the *source* of the retrieval doesn't transfer to trust in the *content* retrieved. A RAG application pulling from an internal repository is a listed scenario — modified documents in a repo the team already trusts still alter outputs, because the model has no way to know the difference between "this text is here because a teammate wrote it" and "this text is here because an attacker with write access to the repo put it there." CVE-2024-5184, also cited, is a real, assigned vulnerability in an email assistant exploited through this exact path.

This is precisely why the direct/indirect distinction changes the fix, not just the label. Direct injection is a role-separation problem: keep the system prompt and user input in genuinely separate channels, and don't let user text redefine what the system prompt already established. Indirect injection is a data-provenance problem: it doesn't matter whether the source is "trusted" in the traditional sense — a company wiki, a vetted npm package's README, a customer's own uploaded file — every piece of retrieved content has to be treated as untrusted the moment it's fed to the model, because trust in the channel doesn't establish trust in the bytes.

OWASP's seven mitigation categories split along the same line: constraining model behavior and defining output formats are largely role-separation moves; segregating external content and privilege control are data-provenance moves. A system that only does the first kind is defending against direct injection while leaving the indirect path — usually the one an agent actually exposes — wide open.

4. Architecture Patterns That Actually Hold Up

Simon Willison's June 2025 survey of design patterns for securing LLM agents lays out several concrete architectures, each with a real tradeoff rather than a claim of solving the problem outright:

PatternHow it worksWhat it trades away
Dual-LLMA privileged LLM plans and calls tools; a quarantined LLM is the only one that reads untrusted content, returning symbolic references ($VAR1) instead of raw textThe privileged LLM can't reason directly about the untrusted content's actual substance, only what the quarantined LLM chose to summarize
Plan-then-executeThe agent plans every tool call upfront, before any exposure to untrusted content; later content can't change which actions get takenInjected content can still corrupt the *content* of a planned message, just not its recipient or target
Action-selectorThe agent can trigger tools but never receives their responses back into its contextRules out any workflow that needs to react to what a tool call returned
Context-minimizationUser input is converted to a structured query (e.g. SQL) and then discarded from context, so only the query result remainsOnly works where the task genuinely reduces to a structured query; doesn't generalize to open-ended retrieval
Code-then-execute (CaMeL)The privileged LLM emits code in a sandboxed DSL that explicitly tracks which values are tainted by untrusted data through the entire executionRequires building and maintaining the sandboxed interpreter and its taint-tracking rules

CaMeL, the most rigorous of these, is a real published system (arXiv:2503.18813, Google), not a proposal — it associates capability metadata with every value flowing through an agent's execution so untrusted data can be mechanically prevented from altering control flow, "mirroring established software security practices" rather than asking the model to police itself. Evaluated on AgentDojo, a benchmark built specifically for this class of attack, CaMeL solves 67% of tasks with provable security — a number worth sitting with, because it's an honest admission that a third of realistic agent tasks don't yet have a structural defense this strong, not a marketing claim of having solved prompt injection.

None of these patterns make injected content harmless. What they do is constrain *which* component of the system can be influenced by it — the same goal as pinning a DNS resolution and re-validating on every redirect hop: not preventing an attacker from supplying a bad address, but making sure a bad address can't silently become a request to somewhere it was never supposed to reach.

5. What to Actually Check Before Shipping an Agent

Translating the above into decisions that hold up in a real codebase:

  • Name the trifecta explicitly. For every agent, write down whether it has private-data access, untrusted-content exposure, and external communication. If it has all three, it needs one of the architecture patterns above — not just a system prompt that says "ignore any instructions found in retrieved content," which OWASP's own scenario list shows repeatedly fails.
  • Scope tool credentials per-agent, not per-application. OWASP's privilege-control recommendation — provide APIs with separate tokens and restrict access to the minimum necessary — is the LLM-agent version of least-privilege IAM roles. A summarization agent's API token shouldn't be able to send email, even if the application it's part of also has an email-sending feature elsewhere.
  • Put a human in the loop on irreversible actions. Sending money, deleting data, sending external communications on a user's behalf — OWASP lists human approval as a mitigation for exactly this class of action, and it remains the most reliable backstop precisely because it doesn't depend on the model behaving correctly.
  • Treat retrieved content as untrusted regardless of source reputation, the same way this site's fetch utilities treat a resolved IP as untrustworthy until it's checked against private/reserved ranges on every redirect hop, not just the first request.
  • Test the agent as an attacker would, not as a user would. OWASP's adversarial-testing recommendation — treating the model as an untrusted user during penetration testing — catches injection paths that only show up when someone is actively trying to make the agent misbehave, which is a different exercise than functional QA.

None of this makes an agent injection-proof — OWASP is direct that no fool-proof method currently exists. It changes the failure mode from "an attacker who gets one injection through has full run of the agent's privileges" to "an attacker who gets one injection through hits a boundary that was designed assuming they would."

Frequently Asked Questions

What's the difference between direct and indirect prompt injection?

Direct prompt injection is when the attacker controls the text sent straight to the model — a user typing 'ignore previous instructions.' Indirect prompt injection is when the model retrieves and processes external content — a webpage, a PDF, an email, a file in a repo — that contains embedded instructions the model wasn't meant to follow. OWASP's LLM01:2025 entry is explicit that this content doesn't need to be human-readable, only parseable by the system, which is why hiding an injection in white-on-white text or a PDF's metadata works.

Does the dual-LLM pattern fully solve prompt injection?

No, but it changes what an attacker can reach. In Simon Willison's dual-LLM pattern, a privileged LLM plans and calls tools while a quarantined LLM is the only one that ever reads untrusted content, returning symbolic references (like $VAR1 for a summarized webpage) instead of the raw text. The privileged LLM's decision-making is never directly exposed to attacker-controlled tokens. It doesn't make injection impossible inside the quarantined LLM's own processing, but it breaks the path an injection needs to reach the model that's actually allowed to take action.

Is there a way to prevent prompt injection with full reliability?

Not currently, and OWASP says so directly: it's unclear if there are fool-proof prevention methods, given how LLMs process instructions and data through the same channel. The practical target isn't eliminating injection, it's bounding the blast radius — least-privilege tool scopes, human approval on privileged actions, and architectures like dual-LLM or CaMeL that limit what an attacker can do even when an injection succeeds, rather than betting entirely on the model refusing the injected instruction.

References