Worked fix: sanitising output before it renders
Lesson 5 of 5 in The Guardrail Catalog: Runtime Checks, Mapped Per Cloud.
Catalog row 8 gets its own lesson because it is the cheapest entry in the catalog and the one whose absence has produced the largest number of published agent data-leak disclosures.
The mechanism is simple enough to state in one sentence, and a defender needs nothing more detailed than this. Markdown and HTML contain constructs that make the rendering client fetch a URL automatically — an image tag being the classic one. If an agent can be induced to put attacker-chosen text into a URL inside its output, and your UI renders that output as markup, then rendering the answer performs the exfiltration. No click, no download, no malware. The trifecta’s third leg — external communication — turns out to have been sitting in your renderer the whole time.
That is why this row pairs with a containment truth: the same chain needs an injection to steer the agent and private data in context, so injection filtering and least-privilege context both cut it too. But sanitisation is the layer that closes the channel, and closing the channel is what the vendors below ended up doing.
| Disclosure | Injection entry point | Exfiltration channel | The fix that shipped | What it teaches |
|---|---|---|---|---|
Slack AI (2024-08-20) | Message in an attacker-created one-member public channel, retrieved as context. | Rendered markdown link with the secret as a URL parameter — required a victim click. | Slack said it deployed a patch; specifics were not in the disclosure. Its stated position was that public-channel visibility is intended behaviour. | “It needs a click” is not a mitigation. Users click links that say reauthenticate. |
GitLab Duo (2025-05-22) | Hidden prompts in MR descriptions, comments, commit messages, issues, source code. | Untrusted HTML rendered in Duo’s responses. | Patch prevented Duo rendering unsafe HTML tags such as | Tag-level sanitisation plus a host allowlist — the first genuinely structural fix on this list. |
AgentFlayer (2025-08-06) | Uploaded document containing an invisible (1px, white) injection. | Rendered image URL with secrets as parameters. Zero-click. | Bypassed an existing | Reputation-based allowlisting fails. “Is this host trustworthy?” is the wrong question; “may model output cause any fetch?” is the right one. |
ForcedLeak (2025-09-25) | Web-to-Lead form Description field (42,000 characters of attacker-controlled text). | Image URL to a domain on Salesforce’s CSP image allowlist that had expired and was purchasable. | Salesforce re-secured the expired domain and released patches enforcing Trusted URLs, so agent output cannot be sent to untrusted URLs. | An allowlist is an asset with an expiry date and an owner. Nobody was watching the registration. |
CamoLeak (2025-10-08) | Pull-request description using GitHub’s invisible-comments feature. | GitHub’s own Camo image proxy: a pre-generated dictionary of signed URLs, one per character, rendered as invisible images. | GitHub disabled image rendering in Copilot Chat entirely. | Your own signed proxy is not a shield if the signatures can be minted in advance. The end of the ladder is removing the capability. |
So write the catalog entry properly. Risk: improper output handling (OWASP LLM05) used as an exfiltration channel. Check: strip or neutralise every construct that can cause an automatic fetch — image tags, forms, iframes, CSS url(), SVG references, link previews — and allowlist any host you still permit, by explicit registration you control. Where it runs: at the rendering boundary, in every channel that renders model output. Cost: microseconds, no model call, no false positives worth the name.
The phrase doing the work is every channel. Teams sanitise the main chat pane and forget that the same agent output reaches a PR comment, an email, a Slack message with link unfurling, or a webhook consumer that renders HTML. Enumerate your render surfaces before you tune your thresholds.
One nuance that changes the design, and that the disclosures above make concrete: with an image-tag channel, the outbound request is made by the victim’s browser, not by your agent’s host. Server-side egress controls do not close it. They do close the service-side variant, where the agent itself fetches the attacker URL — so you need both, and you need to know which one each control covers.
Interactive sorting exercise: Eight proposed responses to the markdown-image exfiltration channel. Which actually close it, which only narrow it, and which do not close it at all?
Interactive flashcard deck.
Interactive checkpoint quiz (2 questions) — open this page in a browser to take it.