Running code in a sandboxed iframe safely
What the sandbox attribute actually restricts, why allow-scripts and allow-same-origin together defeat the whole thing, and how a live preview stays contained.
Last reviewed · 1,449 words
In short
- The sandbox attribute removes every capability by default and re-grants them one token at a time.
- allow-scripts and allow-same-origin together let the framed page remove its own sandbox. Never use both.
- A sandboxed iframe with no same-origin token gets an opaque origin, so it cannot read cookies or storage from the parent site.
- srcdoc runs content without a network request, which is what makes a live preview instant.
- Content Security Policy and the sandbox are separate layers, and a serious preview uses both.
Running arbitrary HTML, CSS and JavaScript in a page is a live preview, and it is also arbitrary code execution. The iframe sandbox is what separates the two.
What the sandbox does
Adding the sandbox attribute to an iframe removes every capability: no scripts, no forms, no popups, no plugins, no navigation of the parent, no downloads, and an opaque origin so the content cannot access the parent's cookies, localStorage or DOM.
Capabilities are then re-granted individually:
| Token | Grants |
|---|---|
allow-scripts | JavaScript execution |
allow-forms | Form submission |
allow-popups | window.open |
allow-modals | alert, confirm, prompt |
allow-same-origin | Treats the content as same-origin |
allow-top-navigation | Navigating the parent page |
allow-downloads | Triggering downloads |
allow-pointer-lock | Locking the cursor |
The design is correct: deny everything, grant what is needed. Adding a token is a deliberate decision with a stated reason.
The combination that defeats it
allow-scripts and allow-same-origin together remove the protection entirely.
With both, the framed content runs JavaScript and is treated as same-origin with the parent. Same-origin means it can reach into the parent document — and, specifically, it can find its own iframe element and remove the sandbox attribute, then reload itself with no restrictions at all.
The specification warns about this explicitly. It is not a subtle interaction; it is the documented consequence of granting both, and it appears in real code regularly because each token looks reasonable in isolation.
For a code preview, allow-scripts is necessary and allow-same-origin is not. Omitting the second gives the content an opaque origin: it can run JavaScript, and it cannot read the parent's cookies, localStorage, sessionStorage or DOM. That is exactly the containment a preview needs.
The cost is that the sandboxed content cannot use localStorage or cookies itself, since it has no origin to key them to. For a preview that is acceptable, and it should be stated rather than left to surprise the user.
srcdoc, and why previews use it
Two ways to put content in an iframe.
src points at a URL, requiring a network request.
srcdoc contains the HTML directly as an attribute value.
srcdoc is what makes a live preview instant — no request, no round trip, and the content updates as fast as the attribute can be set. It respects the sandbox identically.
The alternative for larger content is a blob URL, created from the content in memory. It also avoids a network request and gives the content a URL, which some APIs need.
Either way, updating the preview means replacing the whole document rather than mutating it, which is why a live preview resets scroll position and any state on each keystroke. Debouncing the update by a few hundred milliseconds is the usual mitigation.
Content Security Policy
The sandbox controls what the framed content may do. CSP controls what it may load.
A restrictive policy for a preview blocks external requests, so the framed code cannot call out to a third-party server, load a remote script, or exfiltrate anything typed into it.
The two are independent layers and a serious implementation uses both. The sandbox stops the content escaping upward into the parent; CSP stops it reaching outward across the network.
Note the interaction that catches people: a CSP on the parent page does not automatically apply to a sandboxed iframe with an opaque origin. The policy has to be delivered to the frame itself, which for srcdoc content means a meta tag inside the content or the csp attribute on the iframe.
What can still go wrong
A sandbox is containment, not a guarantee. Several things remain possible.
Resource exhaustion. An infinite loop in the framed script blocks its own thread and, on some browsers, degrades the whole tab. A preview cannot easily prevent this, and the practical answer is that the user closes the tab.
Memory. Allocating unbounded memory can crash the tab.
Visual deception. The framed content controls its own rendering and can be made to look like part of the parent page. This is why a preview should be visually delimited rather than seamless.
Browser bugs. Sandbox escapes have been found and patched. Depth matters: sandbox plus CSP plus, for anything running untrusted third-party code, a separate origin entirely.
For a tool where the user runs their own code, the threat model is limited — they are not attacking themselves. The sandbox matters because a shared link means someone else's code runs in your browser, and that is a genuine risk worth containing.
Sharing a snippet safely
Any code playground that allows sharing has to decide how the code travels.
In the URL fragment, usually compressed and base64 encoded. The fragment is never sent to the server, so the code stays private to whoever holds the link. Limited by URL length, which browsers cap somewhere between 2,000 and 64,000 characters.
Stored server-side with a short identifier. Handles any size, and it means the operator holds the code — which needs saying, since people paste credentials into playgrounds without thinking.
Not at all, which is the most private option and the least useful.
Whichever is used, opening a shared snippet means running a stranger's JavaScript. The sandbox is what makes that acceptable, and it is why the two-token combination above is worth being categorical about.
Building a preview well
A short list of things that separate a usable preview from an irritating one.
Debounce the update, 300 to 500 ms after typing stops. Re-rendering on every keystroke destroys scroll position and burns battery.
Preserve scroll where possible, by restoring the position after the replacement.
Show errors. Catching window.onerror inside the frame and surfacing the message in the parent is the single most useful feature, because a blank preview with no explanation is the common failure.
Provide a console. Capturing console.log from inside the frame and displaying it turns a preview into something you can debug in.
Persist the code locally, so a refresh does not lose work. localStorage in the parent, not in the frame, since the frame has no origin.
Say what the sandbox forbids. A user whose code fails because fetch is blocked should be told that, not left to guess.
What runs where
A browser-based playground executes everything on the client, which shapes what it can and cannot do.
HTML, CSS and JavaScript run natively. The browser is the runtime, so there is nothing to install and nothing to send anywhere.
TypeScript, JSX, Sass and similar need transpiling first, which a playground does in the browser with a compiler shipped as a library. It works and it adds significant page weight.
Anything server-side — PHP, Python, Ruby — cannot run in a browser at all. A playground offering them is executing the code on a server, which is a different privacy proposition and worth knowing before pasting anything sensitive.
WebAssembly has changed the boundary. Full Python, SQLite and even a Linux kernel now run in the browser compiled to WebAssembly, entirely client-side. It is slower than native and it keeps the privacy property, which for a playground is a good trade.
Debugging inside the frame
A preview is much more useful once errors are visible, and the mechanism is straightforward.
Catch errors inside the frame. A window.onerror handler and an
unhandledrejection listener injected into the framed document can post the
message to the parent with postMessage, which then displays it.
Capture console output by replacing console.log, warn and error inside
the frame with functions that forward to the parent before calling the original.
Use postMessage for everything crossing the boundary, and validate the origin
on receipt. A sandboxed frame with an opaque origin posts as "null", so the
check has to account for that rather than comparing against the parent's origin.
Do not try to read into the frame from the parent. With no allow-same-origin
token that access is blocked, which is the whole point — the frame reports out
rather than the parent reaching in.
The result is a preview that shows a syntax error as a message rather than as a blank rectangle, which is the difference between a tool people use and one they abandon.
What this tool assumes
- The preview runs in an iframe with
allow-scriptsand deliberately withoutallow-same-origin, so it cannot reach the parent page. - The framed content has an opaque origin, so cookies, localStorage and sessionStorage are unavailable inside it.
- Content is injected with
srcdoc, which avoids a network request and updates instantly. - Your code stays in your browser. Nothing is uploaded or stored on a server.
- Long-running or infinite loops will freeze the preview; reloading the page clears it.