Skip to content

Commit c7ef5f0

Browse files
ochafikclaude
andauthored
fix: Add missing origin parameter to PostMessageTransport default constructor, verify origins in example sandbox proxy (#207)
* 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. * 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. * validate messages from app come from same origin as sandbox proxy * fix(basic-host): be resilient to individual server connection failures 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 <[email protected]> --------- Co-authored-by: Claude <[email protected]>
1 parent 7837a8c commit c7ef5f0

File tree

3 files changed

+56
-6
lines changed

3 files changed

+56
-6
lines changed

examples/basic-host/src/index.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -350,7 +350,27 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
350350
async function connectToAllServers(): Promise<ServerInfo[]> {
351351
const serverUrlsResponse = await fetch("/api/servers");
352352
const serverUrls = (await serverUrlsResponse.json()) as string[];
353-
return Promise.all(serverUrls.map((url) => connectToServer(new URL(url))));
353+
354+
// Use allSettled to be resilient to individual server failures
355+
const results = await Promise.allSettled(
356+
serverUrls.map((url) => connectToServer(new URL(url)))
357+
);
358+
359+
const servers: ServerInfo[] = [];
360+
for (let i = 0; i < results.length; i++) {
361+
const result = results[i];
362+
if (result.status === "fulfilled") {
363+
servers.push(result.value);
364+
} else {
365+
console.warn(`[HOST] Failed to connect to ${serverUrls[i]}:`, result.reason);
366+
}
367+
}
368+
369+
if (servers.length === 0 && serverUrls.length > 0) {
370+
throw new Error(`Failed to connect to any servers (${serverUrls.length} attempted)`);
371+
}
372+
373+
return servers;
354374
}
355375

356376
createRoot(document.getElementById("root")!).render(

examples/basic-host/src/sandbox.ts

Lines changed: 31 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,12 @@ if (!document.referrer.match(ALLOWED_REFERRER_PATTERN)) {
1616
);
1717
}
1818

19+
// Extract the expected host origin from the referrer for origin validation.
20+
// This is the origin we expect all parent messages to come from.
21+
const EXPECTED_HOST_ORIGIN = new URL(document.referrer).origin;
22+
23+
const OWN_ORIGIN = new URL(window.location.href).origin;
24+
1925
// Security self-test: verify iframe isolation is working correctly.
2026
// This MUST throw a SecurityError -- if `window.top` is accessible, the sandbox
2127
// configuration is dangerously broken and untrusted content could escape.
@@ -79,8 +85,18 @@ function buildCspMetaTag(csp?: { connectDomains?: string[]; resourceDomains?: st
7985

8086
window.addEventListener("message", async (event) => {
8187
if (event.source === window.parent) {
82-
// NOTE: In production you'll also want to validate `event.origin` against
83-
// your Host domain.
88+
// Validate that messages from parent come from the expected host origin.
89+
// This prevents malicious pages from sending messages to this sandbox.
90+
if (event.origin !== EXPECTED_HOST_ORIGIN) {
91+
console.error(
92+
"[Sandbox] Rejecting message from unexpected origin:",
93+
event.origin,
94+
"expected:",
95+
EXPECTED_HOST_ORIGIN
96+
);
97+
return;
98+
}
99+
84100
if (event.data && event.data.method === RESOURCE_READY_NOTIFICATION) {
85101
const { html, sandbox, csp } = event.data.params;
86102
if (typeof sandbox === "string") {
@@ -112,14 +128,25 @@ window.addEventListener("message", async (event) => {
112128
}
113129
}
114130
} else if (event.source === inner.contentWindow) {
131+
if (event.origin !== OWN_ORIGIN) {
132+
console.error(
133+
"[Sandbox] Rejecting message from inner iframe with unexpected origin:",
134+
event.origin,
135+
"expected:",
136+
OWN_ORIGIN
137+
);
138+
return;
139+
}
115140
// Relay messages from inner frame to parent window.
116-
window.parent.postMessage(event.data, "*");
141+
// Use specific origin instead of "*" to prevent message interception.
142+
window.parent.postMessage(event.data, EXPECTED_HOST_ORIGIN);
117143
}
118144
});
119145

120146
// Notify the Host that the Sandbox is ready to receive Guest UI HTML.
147+
// Use specific origin instead of "*" to ensure only the expected host receives this.
121148
window.parent.postMessage({
122149
jsonrpc: "2.0",
123150
method: PROXY_READY_NOTIFICATION,
124151
params: {},
125-
}, "*");
152+
}, EXPECTED_HOST_ORIGIN);

src/app.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1027,7 +1027,10 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
10271027
* @see {@link PostMessageTransport} for the typical transport implementation
10281028
*/
10291029
override async connect(
1030-
transport: Transport = new PostMessageTransport(window.parent),
1030+
transport: Transport = new PostMessageTransport(
1031+
window.parent,
1032+
window.parent,
1033+
),
10311034
options?: RequestOptions,
10321035
): Promise<void> {
10331036
await super.connect(transport);

0 commit comments

Comments
 (0)