Skip to content

harden: gate host-affecting bridge messages + pin CSP isolation in tests - #75

Merged
benvinegar merged 1 commit into
mainfrom
harden/csp-regression-and-bridge-source
Jun 19, 2026
Merged

harden: gate host-affecting bridge messages + pin CSP isolation in tests#75
benvinegar merged 1 commit into
mainfrom
harden/csp-regression-and-bridge-source

Conversation

@benvinegar

Copy link
Copy Markdown
Member

Why

While stress-testing fresh main (every surface kind, all three integration tiers, and a battery of iframe-escape attempts on real Chromium + WebKit), the isolation held up — but two gaps stood out:

  1. The core invariant was only tested by proxy. Every isolation e2e asserts the sandbox attribute (sandbox="allow-scripts", no allow-same-origin). Nothing asserted the actual behavior — that a contained script can't reach the board API. A regression that put the board origin back into connect-src (or dropped the CSP meta tag) would keep the attribute intact and pass every test while silently opening exfil.

  2. Two bridge handlers were ungated. resize/send-prompt already required the message to come from a recognized surface frame, but switch-session and open-link honored a message from any frame — so a stray or nested frame could drive session navigation or pop an "Open external link?" dialog.

What changed

Bridge gating (viewer/src/App.tsx): switch-session and open-link now require isOwnFrame(ev.source) — a frame the viewer actually embedded. frameForSource only tracks html-part frames, but open-link is also sent by rich-part frames (markdown links route through openLink because target=_blank can't open under allow-scripts alone), so isOwnFrame recognizes any embedded iframe rather than just the html-only registry.

Tests pinning the guarantee directly:

  • unit (test/surfacePage.test.ts): the board origin is never a connect-src/script-src source — only img-src/media-src, for asset embedding. This is the exact exfil hole the existing 'self'/wildcard/https: checks miss (the origin is none of those).
  • e2e (e2e/isolation.spec.ts): script inside an html part is CSP-blocked from fetch('/api/surfaces'), asserted on real Chromium and WebKit via a self-reporting probe.

No CSP change — script-src (inline + CDN allowlist) is untouched; scripts in html parts run exactly as before. This only tightens the host-side bridge and adds coverage.

Validation

  • npm test — 158 pass · npm run typecheck · npm run lint · npm run format:check all clean
  • npm run test:e2e isolation (2), viewer incl. send-prompt/comment (34), markdown + diff (4) — pass on both browsers

🤖 Generated with Claude Code

…on in tests

The postMessage bridge honored switch-session and open-link from any frame,
while resize/send-prompt were already gated to a recognized surface frame.
Gate the former two on isOwnFrame(ev.source) so a stray or nested frame can't
drive session navigation or pop an open-link dialog. switch-session is sent
only by html frames, but open-link is also sent by rich-part frames (which are
not in the html-only frameForSource registry), so isOwnFrame recognizes any
iframe the viewer embedded.

Also pin the load-bearing isolation guarantee directly, where it was only
covered by the sandbox attribute as a proxy:
- unit: the board origin is never a connect-src/script-src source (img/media
  only) — the exact exfil hole that 'self'/wildcard/`https:` checks miss.
- e2e: script inside an html part is CSP-blocked from fetching the board API,
  asserted on real Chromium and WebKit via a self-reporting probe.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@benvinegar
benvinegar merged commit fdf4ffc into main Jun 19, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant