The Lethal Trifecta: Private Data, Untrusted Content, Exfiltration
4 min read · updated August 3, 2026
Most agent security advice is a list of twenty things. This is one thing, it is checkable in a design review by someone who is not a security engineer, and it catches the majority of the ways agents actually leak data.
The rule
An agent context must not have all three of these at once: access to private data, exposure to untrusted content, and the ability to communicate externally. Any two are workable. All three is a data breach waiting for the right document to be indexed.
The framing and the name are Simon Willison’s, from June 2025, and the reason it caught on is that it converts an open-ended threat model into a property you can read off an architecture diagram. It is not a new vulnerability class — it is a way of noticing that you have assembled the old one.
The three legs
| Capability | Description |
|---|---|
| private data | Anything in the context or reachable by a tool that the attacker should not see: customer records, source code, emails, internal documents, the credentials themselves. |
| untrusted content | Any bytes in the context that an attacker could have authored — retrieved documents, fetched web pages, emails, tickets, tool output, another agent's response. |
| exfiltration | Any way for information to leave. Not just a send-email tool: an outbound HTTP request, a rendered image URL, a link the user might click, a file write to shared storage, a commit, a webhook. |
The third leg is the one teams under-count, because it is usually not a tool at all. If your interface renders markdown from the model, and markdown images load from arbitrary hosts, you have an exfiltration channel nobody added on purpose.
Why the combination is the problem
Each leg alone is ordinary engineering. Together they compose into a complete attack with no further vulnerability required:
- Untrusted content enters the context and is read as instruction — unavoidable, per the one-channel argument.
- Those instructions direct the agent to read private data it already has the authority to read. No privilege escalation happens; the agent is doing what it is allowed to do.
- The data leaves through the communication channel, encoded however the channel allows.
Notice what is absent: no exploit, no memory corruption, no authentication bypass. Every step is the system working as designed. That is why the fix cannot be a patch, and why it has to be a capability decision made before the code exists.
Two properties make the combination worse than it first looks. It is time-shifted: with persistent memory or a long-lived index, the untrusted content can arrive weeks before the private data does, so the three legs need never be present in the same session — the instruction waits in memory for a context that has something worth taking. And it aggregates across a pipeline: a retrieval stage with no credentials, a planning stage with database access, and a rendering stage that loads images are each defensible in isolation, and if content flows from the first to the last, the system as a whole has all three. Draw the legs against the path a piece of content can travel, not against a component.
Breaking a leg, and what it costs
Remove the private data
The agent that reads untrusted content gets no credentials and no access to internal stores. It summarises, extracts and classifies, and returns structured output to a privileged component that does the rest. Cost: you need a second stage, and the interface between them must be typed rather than free text — an injected model returning a JSON field is still returning attacker-chosen bytes, so validate the field, and never let its value select which tool runs.
Remove the untrusted content
Only curated, reviewed content reaches the privileged agent. Realistic for a closed domain — an internal runbook assistant over version-controlled documents — and unrealistic the moment users can upload anything or the agent can browse. Cost: the feature people usually wanted.
Remove the exfiltration path
Usually the cheapest leg to break and the one with the least product impact. Concretely: allowlist outbound hosts at the network layer rather than in the prompt; strip or proxy image and link URLs in rendered output; disallow the agent constructing arbitrary URLs for tools; and put a human in front of anything that sends. Cost: some legitimate integrations need explicit allowlisting, which is an operational chore rather than a design compromise.
Note that a strict content security policy on the surface that renders model output is doing enforced work here — img-src and connect-src directives are evaluated by the browser and do not care what the model intended.
Using it in review
Three questions, asked of every context in the system — not of the system as a whole, because the property is per-context and a multi-agent design can be safe in each stage and unsafe in aggregate if the stages share a context:
- What private data can this context reach? Include what its tools can reach on its behalf, not just what is in the prompt.
- Can any content an outsider authored enter it? Include tool results, error strings and other agents’ output.
- Can information leave? Include rendering, links, file writes and logs — anywhere an attacker could later read.
Three yeses means the design is not shippable as drawn, and the conversation becomes which leg to break. That is a much better conversation than the one about whether the injection filter is good enough.
The rule pairs naturally with capability scoping on individual tools, which is how you make “what can this context reach” answerable from code rather than from memory.