The sandboxing model

How host-authored HTML/CSS/JS runs without ever becoming a way to attack this site or another host's page.

Two surfaces let a host write raw HTML/CSS/JS: the custom-html block (src/lib/blocks/blocks/custom-html.tsx) and the whole-page escape hatch (src/lib/blocks/custom-page-frame.tsx). Both build their document with the same function and render it into an iframe with the same sandbox attribute — there is exactly one place this trust boundary is defined.

buildSandboxSrcDoc

buildSandboxSrcDoc (src/lib/blocks/sandbox.ts:27) inlines the host’s html/css/js into one document with no external script or stylesheet loading exposed — nothing here can reach a third-party origin. Before inlining, it runs the host’s css and js through escapeClosingTags (src/lib/blocks/sandbox.ts:8), which rewrites a literal </style or </script inside the host’s own content to <\/style/<\/scriptso it can’t early-close the tag it’s embedded in and get dumped into the page as inert text. This is a correctness fix for the host’s own snippet, not a trust-boundary fix — the content is still the host’s own code inside their own already-sandboxed iframe either way.

The iframe boundary

Both call sites render with sandbox="allow-scripts" and nothing else — allow-same-origin is never set. Confirmed at both: src/lib/blocks/blocks/custom-html.tsx:112 (the per-block frame) and src/lib/blocks/custom-page-frame.tsx:39 (the whole-page frame).

The one sentence that matters

allow-scripts without allow-same-originmeans the frame gets a unique opaque origin on every render, with no access to this site’s cookies, localStorage, or the parent window’s DOM — scripts run, but they run somewhere that can’t reach anything worth attacking.

CSS elsewhere never becomes a <style> tag

Outside the sandboxed iframe, a host can style things too — a block’s customCss, a container’s customStyle, and a page’s own pageStyle (src/lib/blocks/types.ts:31, src/lib/blocks/types.ts:282, and src/lib/blocks/types.ts:311). None of these are ever injected as a raw <style>tag in the real (non-sandboxed) document — that would let arbitrary selectors reach outside the one element they’re meant to style. Instead each is parsed by parseInlineStyle (src/lib/blocks/layout-controls.tsx:36) into a plain inline style object with no selectors, applied only to that one element’s own box.

CSP-nonce inheritance

A srcDociframe with no CSP of its own inherits its creator document’s policy verbatim — per the CSP3 spec’s “Inherit a Policy” behavior — including any nonce source on script-src. This app moved off a blanket 'unsafe-inline' to a per-request nonce (src/proxy.ts:39 generates it and stamps it onto both the CSP response header and a forwarded x-nonce request header, built into the policy string at src/proxy.ts:26). Server Components read it back through getCspNonce (src/lib/csp-nonce.ts:12), and buildSandboxSrcDocstamps that same nonce onto the host’s own inline <script> (src/lib/blocks/sandbox.ts:30). Without it, the srcDoc frame’s inherited strict script-srcwould block the host’s own script exactly as it would an untrusted injected one. Callers with no script content (e.g. post-submit confirmation frames that only ever pass js: "") can omit the nonce — an empty script body has nothing to block either way.