From 8e30b0f15f15bf739ba5cd3d31f606db0761edbc Mon Sep 17 00:00:00 2001 From: Jaynel Patiarba Date: Wed, 24 Jun 2026 20:47:46 +0800 Subject: [PATCH] fix(security): validate postMessage origin in sandbox preview bootstrap 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) --- .../src/web/components/deck/use-deck-editor.ts | 3 +++ .../web/components/sandbox/preview/preview.tsx | 18 ++++++++++++++---- packages/sandbox/shared.ts | 2 +- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/apps/mesh/src/web/components/deck/use-deck-editor.ts b/apps/mesh/src/web/components/deck/use-deck-editor.ts index 53514e693b..4420ad1b58 100644 --- a/apps/mesh/src/web/components/deck/use-deck-editor.ts +++ b/apps/mesh/src/web/components/deck/use-deck-editor.ts @@ -123,6 +123,9 @@ export function useDeckEditor(args: { }; const postToIframe = (msg: DeckHostMessage) => { + // Deck iframes are sandboxed (opaque origin) so we must use "*" here — + // a sandboxed iframe's origin is "null" and no targetOrigin string can + // match it. The iframe's own message listener validates e.source instead. machine.iframe?.contentWindow?.postMessage(msg, "*"); }; diff --git a/apps/mesh/src/web/components/sandbox/preview/preview.tsx b/apps/mesh/src/web/components/sandbox/preview/preview.tsx index 0fb9c2a320..395324f640 100644 --- a/apps/mesh/src/web/components/sandbox/preview/preview.tsx +++ b/apps/mesh/src/web/components/sandbox/preview/preview.tsx @@ -378,19 +378,29 @@ export function PreviewContent() { }; }, [sectionsOpen]); + const getPreviewOrigin = (): string => { + try { + return previewUrl + ? new URL(previewUrl, window.location.href).origin + : window.location.origin; + } catch { + return window.location.origin; + } + }; + const injectVisualEditor = () => { const win = previewIframeRef.current?.contentWindow; if (!win) return; win.postMessage( { type: "visual-editor::activate", script: VISUAL_EDITOR_SCRIPT }, - "*", + getPreviewOrigin(), ); }; const deactivateVisualEditor = () => { const win = previewIframeRef.current?.contentWindow; if (!win) return; - win.postMessage({ type: "visual-editor::deactivate" }, "*"); + win.postMessage({ type: "visual-editor::deactivate" }, getPreviewOrigin()); }; const injectCmsEditor = () => { @@ -398,14 +408,14 @@ export function PreviewContent() { if (!win) return; win.postMessage( { type: "visual-editor::activate", script: CMS_EDITOR_SCRIPT }, - "*", + getPreviewOrigin(), ); }; const deactivateCmsEditor = () => { const win = previewIframeRef.current?.contentWindow; if (!win) return; - win.postMessage({ type: "cms-editor::deactivate" }, "*"); + win.postMessage({ type: "cms-editor::deactivate" }, getPreviewOrigin()); }; const handleViewModeChange = (mode: PreviewViewMode) => { diff --git a/packages/sandbox/shared.ts b/packages/sandbox/shared.ts index f6b17e9007..e477174cc9 100644 --- a/packages/sandbox/shared.ts +++ b/packages/sandbox/shared.ts @@ -50,4 +50,4 @@ export { * Must run before the framework builds its WS — spliced after `` by * `injectBootstrap` in image/daemon/proxy.mjs. */ -export const IFRAME_BOOTSTRAP_SCRIPT = ``; +export const IFRAME_BOOTSTRAP_SCRIPT = ``;