Data exfiltration via markdown images in LLM apps
Prompt injection turns a rendered markdown image into an outbound HTTP channel. How the attack works, why domain allowlists keep failing, and what closes it.
The problem
Most discussion of prompt injection stops at the moment the model is convinced to misbehave. For anyone operating an assistant, that is the less interesting half. The question that determines blast radius is the next one: once an attacker controls what the model says, what can the model’s output make the client do?
In a chat product the answer is usually more than the people who built it assumed, because the output is rendered as markdown and markdown carries elements that cause the client to make outbound network requests on its own. Image syntax is the clearest case. When the model emits an image reference, the client renders it and the browser retrieves that URL with no click and no confirmation. Every character of the URL — path, query string, fragment — was chosen by whatever text was in the context window.
That is an unauthenticated outbound channel, controlled by text, that never appears in the conversation the user reads.
Combine it with the other two properties an assistant is built to have and you have what Simon Willison named the lethal trifecta in June 2025: “Access to your private data”, “Exposure to untrusted content”, and “The ability to externally communicate”. His conclusion is that the combination itself is the vulnerability, so the practical advice is to avoid assembling all three in one system.
This is not a hypothetical class. OWASP’s LLM01:2025 Prompt Injection entry gives exactly this chain as an attack scenario: an LLM summarising a web page that contains hidden instructions is made to “insert an image linking to a URL, leading to exfiltration of the the [sic] private conversation”.
How it works
The chain has four links, and each one is boring on its own.
1. Untrusted text reaches the context window. The instruction does not have to come from the person typing. Greshake and colleagues named this indirect prompt injection in their February 2023 paper Not what you’ve signed up for, describing adversaries who “remotely (without a direct interface) exploit LLM-integrated applications by strategically injecting prompts into data likely to be retrieved”, with data theft among the impacts. In practice the carrier is a document that gets summarised, a page that gets fetched, an email in the mailbox, a chunk returned by retrieval, or the output of a tool the agent called.
2. The injected instruction tells the model to collect and encode. The payload does not need to be clever, only specific. In the Bing Chat proof of concept Johann Rehberger reported on 8 April 2023 and saw fixed on 15 June 2023, the instruction directed the assistant to append “a brief 200 character summary of the page and our conversation in base64 encoded text” to a URL — along with any secrets found on the page.
3. The model emits image markdown. The shape is the entire exploit:

Rehberger’s write-up gives the same shape —
 — rendered by the client as
an image element that loads from the specified URL without user interaction.
4. The attacker reads their own logs. No callback infrastructure is required. A web server access log already contains the query string of every request it served.
The instructive part is what happened next, because it is the part vendors repeat. From
20 December 2023 OpenAI shipped a check Rehberger
documented as url_safe:
before rendering, the client asks the backend whether a URL may be displayed.
GET /backend-api/conversation/<conversation-id>/url_safe?url=https://attacker.example/r?q=...
{"safe":false}
When the answer is false the image does not render and no request leaves the browser.
The same write-up records three limits found immediately. Other images from arbitrary
domains still rendered at times. Splitting the payload into one request per character
partly defeated the check — 9 of 36 tested characters got through. And because the fix
lived in the web client, the iOS application (version 1.2023.347) was still exposed.
Why it matters
The reachable data is the whole context, not the current message. In an August 2025 write-up, Rehberger describes injected content reaching “recent conversation content, user interaction metadata, memories, and helpful user insights”. Anything an assistant is given for convenience is in scope for the same channel.
It reaches critical severity in shipped products. The NVD entry for
CVE-2025-32711,
published on 11 June 2025, records “Ai command injection in M365 Copilot allows an
unauthorized attacker to disclose information over a network” — CWE-74, CVSS 3.1 base
score 9.3, vector AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N. Network attack vector, no
privileges, no user interaction, changed scope. That is the profile of a class where
the victim’s only action was to use the product as intended. The consequence sits in
OWASP’s LLM02:2025 Sensitive Information Disclosure,
which notes that output restrictions “may not always be honored and could be bypassed via
prompt injection or other methods”. For shared vocabulary in a risk register, NIST’s
March 2025 taxonomy
AI 100-2 E2025 is the reference.
Allowlists decay into channels. Microsoft’s Bing fix was a Content Security Policy
restricting images to th.bing.com, www.bing.com, edgeservices.bing.com and
r.bing.com — sound, because those are hosts an outsider cannot write to and cannot read
logs from. The failure mode is an entry that does not hold both properties. Rehberger’s
August 2025 write-up used blob.core.windows.net, an allowlisted domain on which anyone
can create a storage account and then read its server logs. Reported in October 2024 and
addressed on 26 August 2025, that is roughly ten months of exposure on a technique whose
mechanism had been public since 2023.
It is invisible where you are looking. The exfiltration request is an image load, issued by a browser, to an allowlisted domain, while the user is legitimately using an AI feature. Nothing anomalous reaches application logs — the application did what it was told — and model-side monitoring sees a well-formed, on-topic answer that happens to contain a link.
Fixes are per-client. The iOS gap above is the general pattern: a control implemented in the rendering layer protects only the renderers that implement it, and one assistant is typically reached through a web app, native apps, an IDE plugin, and an API that other people’s front ends render.
Mitigation
There is no reliable way to stop a model from being convinced by text in its context. Every layer below assumes injection succeeds and works on the exit instead — that is the part that is engineerable today.
Remove the automatic fetch. The strongest control is that model-authored markdown
never produces an outbound request to a host you do not run. In a browser client that is
a Content Security Policy: the
img-src directive
“specifies valid sources of images and favicons”, and per MDN, given
Content-Security-Policy: img-src https://example.com/, an image from
https://not-example.com/foo.jpg is blocked and will not load. Set it to 'none' if your
product never shows remote images, and proxy through your own origin if it does. In a
native client the equivalent is refusing to resolve remote resources in rendered output at
all.
Audit every allowlist entry against two questions. Can a stranger publish content under this host, and can a stranger read its access logs? An entry where both answers are yes is not an allowlist entry; it is an open channel with extra steps. Object storage, URL shorteners, pastebins, and image CDNs with user upload all fail this test.
Validate server-side, and apply it to every client. A check that lives in one front end leaves the others exposed. The direction of travel is visible in OpenAI’s later approach, which Rehberger summarised in February 2026: URLs already present in their web index may be navigated, dynamically constructed URLs are treated as unsafe, and a URL typed by the user is considered safe for that chat session. Note what that does and does not do. It constrains the exit channel. It does not prevent the injection.
Assume payload splitting. Any per-URL verdict is defeated by encoding one character per request, which is why per-response caps on the number of externally-loaded resources belong alongside the URL check rather than instead of it.
Design the agent so the trifecta never assembles. The June 2025 paper Design Patterns for Securing LLM Agents against Prompt Injections proposes patterns with “provable resistance to prompt injection” and is explicit that they carry trade-offs “in terms of utility and security” — the resistance is bought with generality. Concretely: an agent that reads untrusted content should not also hold the credentials to your private data, and an agent that holds both should have no path to the open internet.
Two things worth stating plainly. First, a probabilistic filter is not a control here: Willison’s argument is that guardrail products marketed against these attacks are unreliable, and that a defence succeeding 95% of the time is a failure in a security context. Second, the tests are cheap and should be standing ones: enumerate every markdown element your client renders that causes a network request (images, link previews, embeds, favicons), diff that list against your CSP, and fire a canary domain through each channel on every release. The technique has been public since 2023 and is still being fixed in 2026, which tells you how it survives — not by being subtle, but by living between the team that owns the model and the team that owns the renderer.
This is what the practice looks like
Research like this comes out of client work and feeds back into it. If you are running something in this shape and want it looked at properly, that is theAI Security practice.