fix(server): sandbox the /s/:id surface document against top-level loads#122
Merged
Conversation
The viewer embeds surfaces in a sandbox="allow-scripts" iframe (opaque origin),
but /s/:id is served from the board's own origin — so a TOP-LEVEL load (open
frame in new tab, an agent-shared link) ran the agent's script in the board
origin, where it could reach same-origin storage or window.open('/') the real
viewer and read it. The iframe sandbox attribute doesn't apply to a direct
navigation, and a meta-tag CSP can't carry a sandbox directive.
Set `Content-Security-Policy: sandbox allow-scripts` as a response header on
/s/:id, forcing the same opaque-origin sandbox however the document loads:
allow-scripts so the bridge runs, never allow-same-origin. Unit test asserts the
header; an e2e test proves a top-level load actually lands in window.origin
"null" (and still renders), so the browser enforcement is real, not just the
header's presence.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
270305c to
58624c7
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
The core invariant is agent script must never execute where it can touch the board origin. The viewer upholds this for the embedded case — every surface iframe is
sandbox="allow-scripts"with noallow-same-origin(opaque origin), and rich parts/comments render via sandboxedsrcdoc. But/s/:id(the html-part document) is served from the board's own origin, and the sandbox lives only on the parent's iframe attribute.So a top-level load of
https://<board>/s/<id>— a user choosing "open frame in new tab", or clicking an agent-shared/s/:idlink — runs the agent's<script>in the board origin. The page's meta-tag CSP blocks the API (connect-src) and frame creation, but:sandboxdirective, andwindow.open('/'), which opens the real viewer same-origin — readable by the agent script, and exfiltratable via animgbeacon (img-src https:).That's a host-origin compromise reachable by getting the user to open one link.
Fix
Set the
sandboxdirective as a response header on/s/:id(the only place it can live):This forces the same opaque-origin sandbox however the document is loaded, matching the iframe's own flags exactly:
allow-scriptsso the bridge still runs, neverallow-same-origin. On a top-level load the document now gets a null origin — it can't read the board's cookies/storage, andwindow.open('/')yields a cross-origin window it can't touch. The embedded path is unchanged (it was already opaque via the iframe attribute; the header is consistent and the intersection is identical)./a/:idwas already safe here (non-image uploads are servedattachment+octet-stream+nosniff, so they download rather than execute), andsrcdocrich parts have no top-level URL./s/:idwas the only same-origin route serving agent-executable HTML.Tests
test/api.test.ts):/s/:idcarries asandbox+allow-scriptsCSP header and neverallow-same-origin.e2e/isolation.spec.ts): a top-level load of/s/:idrenders (#probeshows, so scripts ran) andwindow.origin === "null"— proving the browser actually applies the opaque-origin sandbox, not just that the header is present.isolationsuite 4/4 on Chromium).npm run typecheck,npm run lint,npm run format:check,npm test(205/205) ✅🤖 Generated with Claude Code