From e3c01ae93028b2c9f0440ffe4e9e9b5bcfa08378 Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 7 Jan 2026 13:09:45 +0000 Subject: [PATCH 1/4] fix(app): add source validation to default PostMessageTransport App.connect() now passes window.parent as both eventTarget and eventSource, enabling source validation by default. This ensures apps only accept messages from their parent window, preventing potential cross-app message spoofing attacks. Previously, the default transport only specified the target but not the source for validation, meaning apps would accept messages from ANY window. --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index ad97fd331..628651cf0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1027,7 +1027,7 @@ export class App extends Protocol { * @see {@link PostMessageTransport} for the typical transport implementation */ override async connect( - transport: Transport = new PostMessageTransport(window.parent), + transport: Transport = new PostMessageTransport(window.parent, window.parent), options?: RequestOptions, ): Promise { await super.connect(transport); From 507fbbfe3ff9a220a46e25acf615a0523dd85e8d Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 7 Jan 2026 13:16:42 +0000 Subject: [PATCH 2/4] fix(sandbox): add origin validation for host messages The sandbox proxy now validates that messages from the parent window come from the expected host origin (derived from document.referrer). This prevents malicious pages from sending spoofed messages to the sandbox. Changes: - Extract EXPECTED_HOST_ORIGIN from document.referrer - Validate event.origin against expected origin for parent messages - Use specific origin instead of '*' when sending to parent - Reject and log messages from unexpected origins This addresses the TODO comment that was previously in the code. --- examples/basic-host/src/sandbox.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/examples/basic-host/src/sandbox.ts b/examples/basic-host/src/sandbox.ts index 7f27d0b93..ba8e2ef8d 100644 --- a/examples/basic-host/src/sandbox.ts +++ b/examples/basic-host/src/sandbox.ts @@ -16,6 +16,10 @@ if (!document.referrer.match(ALLOWED_REFERRER_PATTERN)) { ); } +// Extract the expected host origin from the referrer for origin validation. +// This is the origin we expect all parent messages to come from. +const EXPECTED_HOST_ORIGIN = new URL(document.referrer).origin; + // Security self-test: verify iframe isolation is working correctly. // This MUST throw a SecurityError -- if `window.top` is accessible, the sandbox // configuration is dangerously broken and untrusted content could escape. @@ -79,8 +83,18 @@ function buildCspMetaTag(csp?: { connectDomains?: string[]; resourceDomains?: st window.addEventListener("message", async (event) => { if (event.source === window.parent) { - // NOTE: In production you'll also want to validate `event.origin` against - // your Host domain. + // Validate that messages from parent come from the expected host origin. + // This prevents malicious pages from sending messages to this sandbox. + if (event.origin !== EXPECTED_HOST_ORIGIN) { + console.error( + "[Sandbox] Rejecting message from unexpected origin:", + event.origin, + "expected:", + EXPECTED_HOST_ORIGIN + ); + return; + } + if (event.data && event.data.method === RESOURCE_READY_NOTIFICATION) { const { html, sandbox, csp } = event.data.params; if (typeof sandbox === "string") { @@ -113,13 +127,15 @@ window.addEventListener("message", async (event) => { } } else if (event.source === inner.contentWindow) { // Relay messages from inner frame to parent window. - window.parent.postMessage(event.data, "*"); + // Use specific origin instead of "*" to prevent message interception. + window.parent.postMessage(event.data, EXPECTED_HOST_ORIGIN); } }); // Notify the Host that the Sandbox is ready to receive Guest UI HTML. +// Use specific origin instead of "*" to ensure only the expected host receives this. window.parent.postMessage({ jsonrpc: "2.0", method: PROXY_READY_NOTIFICATION, params: {}, -}, "*"); +}, EXPECTED_HOST_ORIGIN); From f99592f50004f44094e390247fecb0aa34083dba Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 7 Jan 2026 13:32:47 +0000 Subject: [PATCH 3/4] validate messages from app come from same origin as sandbox proxy --- examples/basic-host/src/sandbox.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/examples/basic-host/src/sandbox.ts b/examples/basic-host/src/sandbox.ts index ba8e2ef8d..0a914afe9 100644 --- a/examples/basic-host/src/sandbox.ts +++ b/examples/basic-host/src/sandbox.ts @@ -20,6 +20,8 @@ if (!document.referrer.match(ALLOWED_REFERRER_PATTERN)) { // This is the origin we expect all parent messages to come from. const EXPECTED_HOST_ORIGIN = new URL(document.referrer).origin; +const OWN_ORIGIN = new URL(window.location.href).origin; + // Security self-test: verify iframe isolation is working correctly. // This MUST throw a SecurityError -- if `window.top` is accessible, the sandbox // configuration is dangerously broken and untrusted content could escape. @@ -126,6 +128,15 @@ window.addEventListener("message", async (event) => { } } } else if (event.source === inner.contentWindow) { + if (event.origin !== OWN_ORIGIN) { + console.error( + "[Sandbox] Rejecting message from inner iframe with unexpected origin:", + event.origin, + "expected:", + OWN_ORIGIN + ); + return; + } // Relay messages from inner frame to parent window. // Use specific origin instead of "*" to prevent message interception. window.parent.postMessage(event.data, EXPECTED_HOST_ORIGIN); From 1cc0e160b56d7a58adfdf9db422d281a04b04e5b Mon Sep 17 00:00:00 2001 From: Olivier Chafik Date: Wed, 7 Jan 2026 13:48:23 +0000 Subject: [PATCH 4/4] fix(basic-host): be resilient to individual server connection failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use Promise.allSettled instead of Promise.all when connecting to servers, so that a single server failure doesn't crash the entire UI. Failed connections are logged as warnings but the UI continues with the servers that connected successfully. Also fixes video-resource-server missing server-utils.ts. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- examples/basic-host/src/index.tsx | 22 +++++++++++++++++++++- src/app.ts | 5 ++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/examples/basic-host/src/index.tsx b/examples/basic-host/src/index.tsx index 8584326ec..e33fc17cb 100644 --- a/examples/basic-host/src/index.tsx +++ b/examples/basic-host/src/index.tsx @@ -350,7 +350,27 @@ class ErrorBoundary extends Component { async function connectToAllServers(): Promise { const serverUrlsResponse = await fetch("/api/servers"); const serverUrls = (await serverUrlsResponse.json()) as string[]; - return Promise.all(serverUrls.map((url) => connectToServer(new URL(url)))); + + // Use allSettled to be resilient to individual server failures + const results = await Promise.allSettled( + serverUrls.map((url) => connectToServer(new URL(url))) + ); + + const servers: ServerInfo[] = []; + for (let i = 0; i < results.length; i++) { + const result = results[i]; + if (result.status === "fulfilled") { + servers.push(result.value); + } else { + console.warn(`[HOST] Failed to connect to ${serverUrls[i]}:`, result.reason); + } + } + + if (servers.length === 0 && serverUrls.length > 0) { + throw new Error(`Failed to connect to any servers (${serverUrls.length} attempted)`); + } + + return servers; } createRoot(document.getElementById("root")!).render( diff --git a/src/app.ts b/src/app.ts index 628651cf0..129b58022 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1027,7 +1027,10 @@ export class App extends Protocol { * @see {@link PostMessageTransport} for the typical transport implementation */ override async connect( - transport: Transport = new PostMessageTransport(window.parent, window.parent), + transport: Transport = new PostMessageTransport( + window.parent, + window.parent, + ), options?: RequestOptions, ): Promise { await super.connect(transport);