Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion examples/basic-host/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,27 @@ class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
async function connectToAllServers(): Promise<ServerInfo[]> {
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(
Expand Down
35 changes: 31 additions & 4 deletions examples/basic-host/src/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ 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;

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.
Expand Down Expand Up @@ -79,8 +85,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") {
Expand Down Expand Up @@ -112,14 +128,25 @@ 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.
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);
5 changes: 4 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1027,7 +1027,10 @@ export class App extends Protocol<AppRequest, AppNotification, AppResult> {
* @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<void> {
await super.connect(transport);
Expand Down
Loading