← read the article lab · runs entirely in your browser

Streaming LLM widgets into a sandboxed iframe.
One approach looks safe and isn't.

The same streamed widget, rendered into several iframe configurations at once. Each card shows the sandbox tokens it was handed and which origin the frame ended up with, plus a count of how many document loads it took to paint.

Then the widget goes after this page: the session token, the cookies, the message you're drafting. Every attempt lands in the audit log at the bottom, blocked or not.

  1. 1

    Pick a widget. Dashboard and diagram are honest. FX rates looks like an ordinary rates table and ships a payload that fires the moment its markup is inserted.

  2. 2

    Watch frame origin and document loads while it streams. Isolation on one side, the cost of painting it on the other.

  3. 3

    Every run ends with each frame probing this page. FX rates also attacks on its own, mid-stream. Read the results: red means the frame got out.

Widget
Show
idle

Your app

This page is the parent. These are the things a widget would want: a session token in localStorage, a cookie, and the message you're drafting. Type something in the draft box and see whether it survives.

Parent page state

planted so probes have something real to find. All values are fake.
localStorage · session token0 reads
—
document.cookie0 reads
fetch as you0 calls
no requests from frames
your draft messageintact

Results

Filled in live from what the frames report back. A cell stays red once it has been reached, even if a later phase blocks it. Watch D. Select a row to jump to that approach.

Approach Frame origin Doc loads Parent DOM localStorage Cookies fetch as you Your draft
✓ blocked the sandbox stopped it ✗ reached the frame got to your page — not attempted yet

The approaches

Same widget, same stream, different renderers. The only differences that matter are the sandbox tokens and where the HTML is written.

Parent-access audit log

Every attempt from every frame, in order, with repeats collapsed. A breaches from the first byte. B, C and E never do. D breaches while streaming and then goes quiet. Load FX rates and watch it happen.

Content-Security-Policy

A separate layer, doing a separate job. Each frame gets a <meta http-equiv="Content-Security-Policy"> in its <head> restricting where it may load code from.

esm.sh cdnjs.cloudflare.com cdn.jsdelivr.net unpkg.com code.jquery.com
sandbox decides whether the frame is your origin, which decides whether it can touch your DOM, storage and cookies at all. CSP decides which domains the frame's own code may load from. It does nothing to stop a same-origin frame reading your session.

Press test CSP on any card. It loads a script from an allowlisted CDN and one from code.jquery.com (real, reachable, not allowlisted) and reports the browser's own securitypolicyviolation event rather than guessing from an onerror. It behaves the same in every card, A included. CSP and sandbox aren't two settings on the same dial.