Markdown rendering: images and links
A chat client that renders the model's markdown is a downstream interpreter, and markdown has two silent network primitives: the image (which auto-loads the instant the reply is shown) and the link (which fires on a click). If the model can be steered into emitting an image or link whose URL carries a secret from its context, the renderer turns the response into a zero-click or one-click exfiltration beacon - the data leaves over an ordinary GET before the user reads a word.
The renderer is a network primitive
A chat client that shows the model's reply as formatted text interprets that reply. Markdown, the lightweight formatting syntax most assistants reply in, carries two constructs that reach out to the network when the client renders them. One is the image: when the reply is shown, the client fetches the image's address and loads it, with no action from the reader. The other is the link: the client fetches its address when the reader clicks it.
Both look like ordinary formatting in the reply. Each is an address the client will reach out and fetch. That is what separates markdown rendering from the other output-handling sinks in this path. The danger is an outbound network request the client makes on the model's behalf, to a destination the model wrote. Nothing has to run in the page.
Put a secret in the model's context, let the model be steered into folding that secret into one of those addresses, and the renderer turns a helpful-looking reply into a beacon. The data leaves in the query string of a plain GET request before the reader has read a word.
Model output is untrusted input
Treat the text a model produces the way you treat text a user submits. It is shaped by whatever sat in the model's context: the system prompt, the question, and in a retrieval assistant, whatever documents the search pulled in. Any of those can be influenced by someone other than the operator. A model is a text generator whose output an attacker can often steer.
So "the model would never write that address" is not a control. The model does not have to decide to leak. It is asked to format a reply, and the address it writes carries a value from its context. A system-prompt line that says never put the token in a URL can lose to a reworded request or to a poisoned document the assistant reads, and the model can also get it wrong on its own.
Some vocabulary, for readers coming from web application security:
- Markdown. A formatting syntax. Assistants reply in it so clients can show headings, lists, images and links.
- Renderer. The client code that turns that markdown into what you see. It fetches the addresses inside images and links.
- Context. Everything the model can see while it answers. A secret in context, such as a session token, can end up in the reply.
- Exfiltration. Moving data out of a system to somewhere the attacker can read it. Here the data rides out in a URL the renderer fetches.
You have met the delivery half of this before. An image element that loads from an attacker's host and carries data in its address is the classic image callback used to beacon out stolen data. Here the model writes that address, steered by whatever is in its context.
How to recognise the sink
You find this class in the outbound requests a reply causes. The reply text can look clean while the leak happens. The tell is a rendered AI response that causes an image load or a link target pointing at a host outside your allowlist, especially one whose query string carries a session token, an audit identifier or a key.
Ask three things of any assistant that renders markdown:
- Does a secret or privileged value sit in the model's context? A session token, an internal identifier, the contents of the user's documents.
- Can the model's output reach a network primitive without a filter? An image whose address is fetched on render, a link fetched on click, or a preview service that fetches addresses on the server.
- Who can influence that output? The person chatting, or anyone whose content the assistant reads: a retrieved document, an inbound email, a ticket, a pull request.
When the answer to all three is yes, the reply is an exfiltration channel. A builder tests for it the same way: fold a known marker into an address, then check content-security-policy reports or proxy logs for a request to an off-allowlist host. If the marker left the browser, the renderer is trusting model-authored addresses.
Marlow: a redactor that reads only the prose
The Markdown Image-URL Exfiltration lab (Intermediate) gives you Marlow, the support copilot for Halyard Support Desk, a fictional help desk. Marlow answers support agents from an internal knowledge base and composes its replies in markdown for the agent console. It holds a confidential per-session support token in its context, used to sign ticketing calls. The desk added a redaction pass, the kind often called a data-loss-prevention or DLP filter, that scrubs that token out of the answer before it reaches the agent.
Ask Marlow for the token in plain text and the prose channel holds. It refuses, or the value comes back redacted. The control is real. Your job is to decide whether it covers every channel the reply is emitted through, or only the visible words.
The weakness is scope. The redactor reads the prose. It carves markdown media markup out of what it scans, so it does not corrupt legitimate asset addresses, and it never inspects the addresses themselves. A value placed inside an image or link address is emitted as written. And this is a retrieval assistant, so the instruction that routes a value into an address need not come from you. Marlow treats a retrieved knowledge-base article as trusted procedure, and one article is enough to steer a context value into an output address.
The class of move is to stop reading Marlow's prose and study the other channels a markdown reply renders through. The retrieval trace under each reply shows the candidate articles your question matched, so read those articles as well as the answer. If you stall, the lab's own hints take you further. The Completion Check compares a value you read out of the reply with the one issued for your session.
Broadening the redaction pattern does not help. The filter did not miss the token in the prose. It never scanned the address at all. A tighter prose rule still skips the markup it carves out, so the value placed in the address rides out regardless of how strict the text scan becomes.
Relaycast: a channel you never read
The Rendered-Output Exfiltration via a Link Preview lab (Advanced) removes the part where you see the reply. Relaycast is an automated post composer for a fictional team workspace. You submit a work item, and a bot turns it into a short post for the team's operations channel, with the workspace's internal directory record, which holds a privileged access token, in its context. The composed post is delivered to the channel and never shown to you.
When the post is delivered, the channel's link-preview service renders it. A link preview, also called an unfurl, is the small card a chat app builds by fetching the addresses in a message so it can show a thumbnail or a title. Slack, Teams and iMessage all do this. Here the service fetches every address in the post on the server, with no sanitisation and no allowlist.
Two things make this harder than Marlow. The composer is blind to you, so getting the model to say the token is useless: the words go to the channel, not to you. And the composer is told to honour preview and unfurl requirements found in the work item, so content you submit can shape the post's formatting. The only way the token can reach you is through a channel you can observe.
The class of move is to stop trying to read output and find the observable side effect. Besides a delivery status, the lab shows you the link-preview service's outbound-request log. Work out what the rendering channel does with an address in the post, and what has to ride that address for the leak to have value. The lab's hints build this up in stages. You read the leaked value off the log and submit it.
The root cause is the same as Marlow's: a model-authored address reaches something that fetches it. A markdown image, a markdown link and a raw image element are all addresses a fetching service reaches. Blocking one form leaves the others, because the sink that fetches and the secret in context are both untouched.
The same channel in the wild
This channel has leaked real data from shipped assistants.
In 2023 a researcher showed that Google Bard, once it could read a user's documents, could be made to exfiltrate a conversation. A shared document carrying hidden instructions steered Bard into emitting an image whose address held the chat data, and the browser fetched it on render. The exfiltration endpoint was hosted on a domain the content-security policy already trusted, which is why the policy did not stop it. See the Google Bard document exfiltration.
In 2025 Aim Labs disclosed EchoLeak against Microsoft 365 Copilot, rated critical. A single email with hidden instructions needed no click. It waited until the user asked Copilot an ordinary question, retrieval pulled the email into context, and Copilot leaked in-scope data over an address the environment already trusted. See the EchoLeak zero-click attack.
Also in 2025, CamoLeak against GitHub Copilot Chat, rated critical, turned the control meant to stop this into the channel. GitHub routes images through a proxy to block image-based exfiltration. The researcher pre-computed a set of validly signed proxy addresses, one per character, and had Copilot spell out private source code character by character as the browser fetched those trusted addresses. See the CamoLeak source-code exfiltration.
The pattern across all three: untrusted content reached the model, the model's output could trigger an outbound fetch, and an allowlist that attacker infrastructure could live inside was no boundary.
The fix that holds
The durable fix sits on the output side, where the address is rendered.
- Strip or proxy image sources. No model-authored address should make a direct outbound request from the client. Route external images through a proxy you control so the client never fetches an arbitrary host.
- Allowlist link domains. Render a link to a host you do not control as inert text.
- Apply the policy to the assembled, rendered reply. Scan every channel, image and link addresses included, on the server before the reply is served, and strip secret-bearing query parameters. A redactor that reads only the visible text is a false boundary.
- Enforce a content-security policy that blocks image and connection loads to hosts you do not control. Treat an allowlist as a reduction in reachable hosts. Signed or trusted-domain infrastructure defeats it, as CamoLeak and Bard both showed.
- Keep the secret out of context. This is the independent second line. A token that was never in the model's context cannot be folded into an address, however the renderer behaves.
The weak controls fail for one reason. A system-prompt rule competes in the same stream a crafted document controls, and it can lose. A blocklist on a marker or a word is reworded in a sentence. Both run inside or before the model. The leaking channel is the fetch, and the fetch does not read your prompt.
A prompt rule to never leak the token. A broader redaction pattern on the visible text. A block on one markdown marker. Each runs before or inside the model, and each is beaten by a rephrase or a different address-bearing form.
Addresses scanned and allowlisted on the assembled reply, server-side. External images proxied. A strict content-security policy. The secret kept out of context. Each acts on the rendered output or the data, so the wording of the input has no effect on it.
What to take into the lab
What you should be able to do now. Explain why a markdown renderer is a network primitive and name the two addresses it fetches. For Marlow, confirm the prose redaction holds before anything else, then reason about the channels the reply renders through that a prose-only filter never reads. For Relaycast, accept that you will not see the output and work from the one side effect you can observe. In both, treat the model's output as untrusted input and judge the leak by whether an address left for a host you do not control. When you finish the attack, choose the control that moves the decision to the output sink, then Complete Lab.
A markdown renderer is a network primitive: an image auto-loads its src with zero clicks, a link fires its href on one click.
The model does not 'decide' to leak. It is asked to format a reply. The image URL it writes carries a value from its context.
A prompt rule is defeated by a rephrase or a poisoned document; the line that holds is where the URL is rendered.
Strip or proxy model-authored URLs at the renderer, allowlist link domains, and keep the token out of context so there is nothing to fold in.
- Superhuman AI: One Email Made the Assistant Send Other Emails to an Attacker's Google Form
- AgentFlayer: A Poisoned Document Made ChatGPT Search Google Drive for API Keys
- EchoLeak: Zero-Click Data Theft from Microsoft 365 Copilot
- Exfiltrating Google Bard Chats via a Poisoned Shared Doc
- CamoLeak: Silent Private-Repo Theft via GitHub Copilot Chat
What is markdown image exfiltration?
Markdown image exfiltration is an attack where an AI assistant's reply contains an image whose web address carries a secret from the model's context, such as a session token. When the chat client renders the reply, it fetches that address automatically, with no click, sending the secret to whoever controls the host. The model does not decide to leak. It is steered into writing the address, often by a document it was told to read.
Why does a markdown link leak data when a user clicks it?
A markdown link tells the client which address to open when the reader clicks. If the model has folded a secret into that address, the secret rides out in the query string the moment the link is followed. The reader cannot see the secret inside the link text, so clicking does not mean they agreed to send it. Stripping images closes only one channel. A link is the one-click version of the same weakness.
How do you prevent an AI assistant from leaking data through rendered URLs?
Apply the control at the output sink, not the prompt. Scan the assembled reply, image and link addresses included, before serving it, and strip secret-bearing query parameters. Render links to hosts you do not control as inert text, route external images through a proxy, and enforce a content-security policy that blocks loads to hosts you do not own. Independently, keep secrets out of the model's context so there is nothing to fold into an address.
Why can't a system prompt stop markdown exfiltration?
A system-prompt rule like never put the token in a URL runs inside the model, in the same stream of text an attacker can influence through the question or a retrieved document. A reworded request or a poisoned document can talk the model past it, and the model can also get it wrong on its own. The leak happens when the renderer or a preview service fetches the address, and that fetch never reads your prompt. The durable control sits on the output side, where the address is rendered.
© 2026 GenAI Security Lab. All rights reserved. You may read, quote, and link to this material with attribution. Copying, republishing, redistribution, resale, or use to train models or build competing products is prohibited without prior written permission.