Skip to content

feat(security): frame-ancestors on the viewer + noreferrer on external links#210

Merged
benvinegar merged 2 commits into
mainfrom
claude/iframe-csrf-headers-rvr3rw
Jul 4, 2026
Merged

feat(security): frame-ancestors on the viewer + noreferrer on external links#210
benvinegar merged 2 commits into
mainfrom
claude/iframe-csrf-headers-rvr3rw

Conversation

@benvinegar

Copy link
Copy Markdown
Member

What

The self-hosted OSS-side counterpart to the hosted iframe-CSRF hardening (sideshow-cloud #172/#173). Closes two gaps in the trusted viewer origin — which shares its origin with the authenticated API and the comment→agent channel.

1. Clickjacking — frame-ancestors 'self' on the viewer HTML.
The viewer HTML now sends Content-Security-Policy: frame-ancestors 'self', set once in the configuredViewerHtml chokepoint so it covers every viewer-HTML route (/, /session/:id, and the standalone /s/:id post view). Without it, a cross-origin page could frame the authenticated viewer and clickjack actions or the prompt-injection channel.

2. Referrer leak — noreferrer on external-link opens.
Self-hosted auth puts the deploy token in the URL (?key=<token>). Anything that sends a Referer from the viewer can leak it. External links the viewer opens now carry noreferrer alongside the existing noopener:

  • viewer/src/App.tsx — the openLink bridge's window.open(...) (arbitrary external destination — the primary vector) and the footer link.
  • viewer/src/notes.ts — release-notes markdown links.
  • viewer/src/ImageSurface.tsx, viewer/src/TraceSurface.tsx — the asset open/download anchors (same-origin today, normalized for consistency).

Deliberately not covered

The sandboxed /s/:id?part=N surface documents are meant to be framed by the viewer and are neutralized by their own sandbox CSP response header. They don't pass through configuredViewerHtml, so they never receive frame-ancestors — verified by test. A bare frame-ancestors directive doesn't restrict script/style-src, so the singlefile viewer's inline scripts/styles are unaffected.

Testing

  • test/api.test.ts: the viewer shell (/s/:id), /, and /session/:id now assert frame-ancestors 'self' and the absence of the sandbox CSP; the existing ?part=N surface-doc assertions (sandbox present) are untouched.
  • test/notes.test.ts (new): pins rel="noopener noreferrer" on rendered external links.
  • Full gate green locally: npm test (422), npm run typecheck, npm run lint, npm run format:check, npm run security:audit; npm run build:viewer compiles the JSX edits cleanly.
  • Changeset added (patch).

Behavior change to note

Cross-origin iframe embedding of the self-hosted viewer HTML is now refused. The sanctioned embed path is the embeddable engine (mountViewer, a JS/shadow-root mount), not iframing the viewer document, so this shouldn't affect real embedders — flagging it explicitly in case any self-hoster relies on iframing the full viewer.

🤖 Generated with Claude Code

https://claude.ai/code/session_017BLz2z8mwodKkzr8x1NapM


Generated by Claude Code

claude added 2 commits July 4, 2026 11:21
…l links

Harden the trusted viewer origin (shared with the authenticated API and the
comment→agent channel) against the two OSS-side gaps flagged alongside the
hosted iframe-CSRF work:

- Clickjacking: the viewer HTML now sends
  `Content-Security-Policy: frame-ancestors 'self'` via the single
  configuredViewerHtml chokepoint (covers `/`, `/session/:id`, and the standalone
  `/s/:id` post view). The sandboxed `/s/:id?part=N` surface documents are meant
  to be framed and keep their own `sandbox` CSP, so they're untouched.
- Referrer leak: external links the viewer opens — the openLink bridge's
  `window.open`, release-notes markdown links, and the image/trace/footer anchors
  — now carry `noreferrer` alongside `noopener`, so the current URL (which can
  hold the `?key=` deploy token) never rides an outbound Referer.

Tests: api.test asserts frame-ancestors on the viewer routes (and its absence of
the sandbox CSP); a new notes.test pins noreferrer on rendered external links.
Changeset: patch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BLz2z8mwodKkzr8x1NapM
…noopener on Card links

Address sonnet review nits on the iframe-CSRF hardening:
- api.test now asserts surface docs (/s/:id?part=N) never carry frame-ancestors
  (the invariant that keeps them framable), and that the nested post-permalink
  alias (/session/:id/p/:id) is frame-ancestors protected like its siblings.
- Card.tsx's two bare target="_blank" links (open-in-tab, open-as-PNG) get
  rel="noopener" for reverse-tabnabbing hygiene. They're same-origin, so no
  noreferrer (keeps the useful same-origin Referer) — the external links keep
  noopener noreferrer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017BLz2z8mwodKkzr8x1NapM
@benvinegar benvinegar merged commit b4d6a42 into main Jul 4, 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.

2 participants