import type { McpUiSandboxProxyReadyNotification, McpUiSandboxResourceReadyNotification } from "../../../dist/src/types"; const ALLOWED_REFERRER_PATTERN = /^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/; if (window.self === window.top) { throw new Error("This file is only to be used in an iframe sandbox."); } if (!document.referrer) { throw new Error("No referrer, cannot validate embedding site."); } if (!document.referrer.match(ALLOWED_REFERRER_PATTERN)) { throw new Error( `Embedding domain not allowed in referrer ${document.referrer}. (Consider updating the validation logic to allow your domain.)`, ); } // 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. try { window.top!.alert("If you see this, the sandbox is not setup securely."); throw "FAIL"; } catch (e) { if (e === "FAIL") { throw new Error("The sandbox is not setup securely."); } // Expected: SecurityError confirms proper sandboxing. } // Double-iframe sandbox architecture: THIS file is the outer sandbox proxy // iframe on a separate origin. It creates an inner iframe for untrusted HTML // content. Per the specification, the Host and the Sandbox MUST have different // origins. const inner = document.createElement("iframe"); inner.style = "width:100%; height:100%; border:none;"; inner.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms"); document.body.appendChild(inner); const RESOURCE_READY_NOTIFICATION: McpUiSandboxResourceReadyNotification["method"] = "ui/notifications/sandbox-resource-ready"; const PROXY_READY_NOTIFICATION: McpUiSandboxProxyReadyNotification["method"] = "ui/notifications/sandbox-proxy-ready"; // Message relay: This Sandbox (outer iframe) acts as a bidirectional bridge, // forwarding messages between: // // Host (parent window) ↔ Sandbox (outer frame) ↔ Guest UI (inner iframe) // // Reason: the parent window and inner iframe have different origins and can't // communicate directly, so the outer iframe forwards messages in both // directions to connect them. // // Special case: The "ui/notifications/sandbox-proxy-ready" message is // intercepted here (not relayed) because the Sandbox uses it to configure and // load the inner iframe with the Guest UI HTML content. // // Security: CSP is enforced via HTTP headers on sandbox.html (set by serve.ts // based on ?csp= query param). This is tamper-proof unlike meta tags. // Build iframe allow attribute from permissions function buildAllowAttribute(permissions?: { camera?: boolean; microphone?: boolean; geolocation?: boolean; clipboardWrite?: boolean; }): string { if (!permissions) return ""; const allowList: string[] = []; if (permissions.camera) allowList.push("camera"); if (permissions.microphone) allowList.push("microphone"); if (permissions.geolocation) allowList.push("geolocation"); if (permissions.clipboardWrite) allowList.push("clipboard-write"); return allowList.join("; "); } window.addEventListener("message", async (event) => { if (event.source === window.parent) { // 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, permissions } = event.data.params; if (typeof sandbox === "string") { inner.setAttribute("sandbox", sandbox); } // Set Permission Policy allow attribute if permissions are requested const allowAttribute = buildAllowAttribute(permissions); if (allowAttribute) { console.log("[Sandbox] Setting allow attribute:", allowAttribute); inner.setAttribute("allow", allowAttribute); } if (typeof html === "string") { // Use document.write instead of srcdoc for WebGL compatibility. // srcdoc creates an opaque origin which prevents WebGL canvas updates // from being displayed properly. document.write preserves the sandbox // origin, allowing WebGL to work correctly. // CSP is enforced via HTTP headers on this page (sandbox.html). const doc = inner.contentDocument || inner.contentWindow?.document; if (doc) { doc.open(); doc.write(html); doc.close(); } else { // Fallback to srcdoc if document is not accessible console.warn("[Sandbox] document.write not available, falling back to srcdoc"); inner.srcdoc = html; } } } else { if (inner && inner.contentWindow) { inner.contentWindow.postMessage(event.data, "*"); } } } 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); } }); // 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);