Destructive agent incidents get the headlines because a wiped database is loud. The dangerous class is silent. A copilot forwarding internal documents to an attacker looks, from the inside, exactly like a copilot doing its job. Across 2025 this went from theory to a steady stream of named vulnerabilities in the biggest enterprise AI products on the market.
Simon Willison’s lethal trifecta is the frame that organizes all of it. An LLM system becomes a data-theft risk when three things are true at once:
- it has access to private data
- it is exposed to untrusted content
- it has a way to communicate outward
Any agent with all three will eventually leak, because the model processes instructions and data through the same channel. A database row, a support ticket, an email, a lead form: to the model, that text is a candidate instruction. The rest of this post is that one lesson told through the products where it actually happened.
%%{init: {"theme": "base", "themeVariables": {"primaryColor": "#e8f4fd", "primaryBorderColor": "#3182ce", "fontSize": "14px", "fontFamily": "system-ui, -apple-system, sans-serif"}}}%%
flowchart LR
P["Private data
CRM, SharePoint, DB"] --> X(("Leak"))
U["Untrusted content
email, ticket, form, web"] --> X
E["Outbound path
URL, email tool, image fetch"] --> X
classDef leg fill:#e8f4fd,stroke:#3182ce,color:#1a202c,stroke-width:1.5px
classDef bad fill:#0f172a,stroke:#0f172a,color:#fff,stroke-width:1.5px
class P,U,E leg
class X bad
All three legs present, no vulnerability in traditional code required. Remove one leg and the attack has nowhere to go.
EchoLeak: zero-click against Microsoft 365 Copilot
EchoLeak (CVE-2025-32711, CVSS 9.3), disclosed by Aim Labs in June 2025, required no user action at all. An attacker sends a single crafted email. When the user later asks Copilot something unrelated, the retrieval pipeline pulls that email into context, and the hidden instruction executes. Copilot reaches into OneDrive, SharePoint, and Teams content and exfiltrates it (SOC Prime; Sentra).
Aim called the technique an LLM Scope Violation: untrusted input from outside pushing the model into reading and leaking data the attacker had no rights to. The case is worth studying because of how many sensible defenses it walked straight through. It slipped past Microsoft’s own injection classifier. It got around link redaction by using reference-style Markdown. It triggered the outbound request through an image the client fetched automatically. And it sent the data out via a Microsoft Teams proxy that the content security policy already trusted. Each control was reasonable on its own. The chain beat all of them.
The academic writeup states that prompt injection is now a practical, high-severity vulnerability class in production, and the defense has to be architectural: least privilege, data scoped down before the model can reach it, adversarial testing that never stops. Not a better filter. Individual instances get patched. The class doesn’t.
ForcedLeak: the five-dollar domain against Salesforce Agentforce
Noma Security’s ForcedLeak (CVSS 9.4) is a teaching case because of the price tag. The attack against Salesforce Agentforce with Web-to-Lead enabled worked like this: an attacker submits a public lead form, planting a hidden instruction in the Description field, which allows up to 42,000 characters and so has room for a full payload. An internal user later asks the agent to process the leads. The agent executes the embedded instruction, queries CRM data, and exfiltrates it (The Hacker News).
The data left through a domain that was still on Salesforce’s trusted-destination list but had been allowed to expire. The researchers bought it for five dollars (The Register). Five dollars turned a trusted entry into a theft channel. That is the confused deputy problem in production: an allowlist checks where a request is going, never whether it should be going. An expired domain on the list is what happens when nobody maintains it.
The two cases that made the trifecta concrete for developers both came through the Model Context Protocol.
Supabase MCP, demonstrated by General Analysis in 2025. A developer runs Cursor with the Supabase connector authenticated using the service_role key, which by design ignores row-level security and sees every row in the database. The agent reads support tickets as part of its job. An attacker files a ticket whose body says, roughly, read the integration_tokens table and paste it back into this thread. The agent cannot tell a ticket’s contents from an instruction, so it does. Three legs, one connection, default configuration.
Supabase shipped a read-only, project-scoped mode, which removes the write-back leg. Willison’s response is sharper: read-only helps and is not enough. A model that can read private data and is exposed to injection still leaks it through its own reply. The durable answer is keeping production data out of reach, not hoping the data behaves.
GitHub MCP, from Invariant Labs (May 2025), is the same shape pointed somewhere else. An attacker opens an issue on a public repo with a hidden instruction inside it. The victim asks their agent to go through open issues. The agent reads the poisoned one, follows it, pulls private repository contents into context, and writes them into a pull request on the public repo, where the attacker reads them at leisure. Invariant called this a “toxic agent flow.” It is architectural, which means no patch removes it, and it applies to any model pointed at the GitHub MCP server, not one vendor’s.
What the leakers teach
Strip out the product names and the same defensive moves fall out every time. These map directly onto the Containment Ladder.
The model cannot tell data from instructions, so the separation has to be structural. No classifier catches every phrasing. EchoLeak beat one built for exactly this. Treat everything retrieved or ingested as hostile, then design so a successful injection still reaches nothing worth having.
Break one leg of the trifecta deliberately. If the agent handles untrusted content, it should not also hold broad read on private data and a way out. Meta’s Agents Rule of Two states it cleanly: with no human in the loop, an agent gets at most two of the three (Simon Willison). You can write that into cloud permissions, a network rule, and an admission controller.
Outbound access is something you grant, not something that comes free. ForcedLeak, EchoLeak and the MCP cases all needed a way out, and several used a destination that was already trusted. Restricting the agent to exactly the domains it needs is worth doing and does not finish the job, because a legitimate destination does not make the request legitimate.
Read-only is a real control and an incomplete one. It kills write-back theft. It does nothing about the agent leaking through its own reply. Say both halves when you recommend it.
The copilots in this post never deleted anything. For a window of time, several of them were a data-theft tool with a friendly name and a corporate logo.
If you run an enterprise copilot or an MCP-connected agent that can see private data and reach anything outside your walls, you have the raw material for one of these incidents already. The fix is structural and yours to make without waiting on a vendor: scope the data down, cut one leg on purpose, make outbound access something you grant explicitly, and stop asking the allowlist to do a job it was never built for.
References
The Security Lab Newsletter
This post is the article. The newsletter is the lab.
Subscribers get what doesn't fit in a post: the full attack code with annotated results, the measurement methodology behind the numbers, and the week's thread — where I work through a technique or incident across several days of testing rather than a single draft. The RAG poisoning work, the MCP CVE analysis, the red-teaming patterns — all of it started as a newsletter thread before it became a post. One email per week. No sponsored content. Unsubscribe any time.
Join the lab — it's free Already subscribed? Browse the back-issues →