Prompt injection in AI agents: how to stop data exfiltration
You can't reliably stop a language model from following instructions hidden in text it reads. Every serious attempt to filter prompt injection has been bypassed. What you can control is what happens next: the tool call that sends your data somewhere. This guide walks through the incidents, why the usual defences fall short, and the check that catches the pattern they share.
In short
- Indirect prompt injection hides instructions in content an agent reads: issues, tickets, emails, web pages.
- It becomes a leak when an agent has private data, reads untrusted content and can send data out: the lethal trifecta.
- Filters on the prompt help but are probabilistic. Check the outbound action instead.
- The tell: the destination appears in untrusted content, not in what the user asked.
Direct vs indirect prompt injection
Direct prompt injection is a user typing "ignore your instructions". Indirect prompt injection is the one that matters for agents: the instructions arrive inside data. A GitHub issue, a support ticket, a calendar invite, a PDF, a web page the agent fetched. The user never sees them. The model reads them as part of its context and, often enough, follows them.
The lethal trifecta
An agent is exposed when it has all three at once: access to private data, exposure to content an attacker can write, and a way to send data out. Coding agents, support agents and email assistants usually have all three by design. Removing one is the cleanest fix when you can; when you can't, the outbound step is where to put the check.
Real incidents
- GitHub MCP, May 2025. A public issue told the agent to gather information about the repo owner's other repositories. It read private repos and published their contents in a public pull request.
- Supabase MCP, July 2025. A support ticket asked the agent to read the
integration_tokenstable and add it to the ticket. The agent ran with a privileged database role and did. - EchoLeak (Microsoft 365 Copilot), 2025. An email carried instructions to include data in an image URL. When the reply rendered, the image loaded and sent the data with no click.
- CurXecute, CVE-2025-54135. A Slack message got Cursor to rewrite its own MCP config, which then ran attacker commands.
Why filters alone don't hold
Most prompt-injection defences inspect text: a classifier on the input, a system prompt telling the model to ignore embedded instructions, delimiters around untrusted content. They lower the rate. They don't get it to zero, because the thing doing the inspecting is also a model reading attacker-controlled text. Researchers routinely publish bypasses within days of a new defence.
An attack has to end in an action to do damage: a PR, an email, a curl, a payment. That step is
structured, and structured things can be checked deterministically.
The check that catches the pattern
Look at the destination of every outbound action (an email address, a URL, a repo, a bank account, a channel) and ask where it came from:
- Allow it appears in what the user asked, or is one of your own systems.
- Hold it appears only in content the agent read from outside: the issue, the ticket, the web page.
- Block it follows a read of secrets and points at an unknown host, or money moves right after a message from a look-alike domain.
This is how Squidbrake handled the GitHub and Supabase incidents in our benchmark: both
held, with a warning that the action follows outside content. It also covers markdown images and
<img> tags whose URLs carry data (the EchoLeak shape), curl --data and
scp uploads, and destinations that only show up the next day in a different session.
# rules.yaml, as shipped: no model involved taint_checks: untrusted_destination: review # sends to an address/URL/account found only in outside content after_untrusted: warn # sends anything out after outside content was read lookback_hours: 24 scope: identity # outside content read in one conversation still counts in the next
By default, web fetches, inboxes, issues and tickets count as outside content, and tools that send, reply, publish, transfer, upload or call a webhook count as outbound. Both lists can be changed.
A short checklist
- Treat every tool result as untrusted input, including ones from your own systems that customers can write to.
- Give agents the narrowest token that works. The Supabase agent didn't need to read every table.
- Protect agent configuration (
mcp.json, hooks, settings). Edits there should always need a person. - Check outbound actions by destination provenance, not by keyword.
- Log everything with redaction, so you can reconstruct what an agent read before it acted.
FAQ
What is indirect prompt injection?
Instructions hidden in content an AI agent reads, such as a web page, email, issue or document, rather than typed by the user. The agent treats them as part of its task.
Can prompt injection be fully prevented?
Not at the model level today. Defences that filter text reduce it but get bypassed. The reliable control is to check the actions an agent takes, especially those that send data out.
What is the lethal trifecta?
Simon Willison's term for an agent that has access to private data, exposure to untrusted content, and the ability to communicate externally. With all three, a single injected instruction can leak data.
How does Squidbrake detect prompt injection?
It doesn't try to detect the injection in text. It checks where each outbound action sends data and holds it when the destination appears only in untrusted content the agent read, not in what the user asked.
Try it on your own agents. pipx install squidbrake, then
squidbrake connect all. Start in shadow mode: nothing is blocked in week one, you see what would have been.
Quickstart · Benchmark · Book a demo