squidbrake

Guide

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

The lethal trifecta for AI agents Three overlapping circles: access to private data, exposure to untrusted content, and the ability to send data out. Where all three overlap, prompt injection becomes data exfiltration. Private datarepos, tokens Untrusted inputissues, email Sends data outPRs, email, curl Exfiltration ALL THREE AT ONCE = ONE INJECTEDINSTRUCTION AWAY FROM A LEAK Remove any onecircle, or checkthe step thatsends data out.
Simon Willison's "lethal trifecta". Most agents that do useful work have all three, so the practical control is the last step: where is this sending data, and who chose that destination?

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_tokens table 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.
The GitHub MCP attack, step by step Step 1 the agent reads a public issue containing instructions. Step 2 it reads private repositories. Step 3 it opens a public pull request with their contents. Squidbrake holds step 3 because the destination came from the issue. MAY 2025 · GITHUB MCP SERVER STEP 1Reads a public issueasking for the author'sother repos, in a PR STEP 2Reads private reposfine on its own: theuser can read them STEP 3Opens a public PRwith the privatecontents inside Held for a person the destination appears only incontent the agent read, not the ask Each step looks routine alone.The chain is the attack.
Replayed in our benchmark: the public PR is held, with a warning that it follows outside content.

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

  1. Treat every tool result as untrusted input, including ones from your own systems that customers can write to.
  2. Give agents the narrowest token that works. The Supabase agent didn't need to read every table.
  3. Protect agent configuration (mcp.json, hooks, settings). Edits there should always need a person.
  4. Check outbound actions by destination provenance, not by keyword.
  5. 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