ext-apps/examples/basic-host/src/sandbox.ts
Olivier Chafik ba550f1aeb
fix: follow-up to PR #234 (remove accidental file, revert from document.write to equiv. srcdoc) (#248)
* chore: remove tamper-detection.ts (not meant to be submitted)

Follow-up to PR #234. This file was accidentally included.

* revert to srcdoc

Follow-up to PR #234 per domfarolino's review feedback.

* regen

* regen
2026-01-12 22:19:31 +00:00

129 lines
5 KiB
TypeScript

import type { McpUiSandboxProxyReadyNotification, McpUiSandboxResourceReadyNotification } from "../../../dist/src/types";
import { buildAllowAttribute } from "../../../dist/src/app-bridge";
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");
// Note: allow attribute is set later when receiving sandbox-resource-ready notification
// based on the permissions requested by the app
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.
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") {
inner.srcdoc = html;
} else {
console.error("[Sandbox] Missing or invalid HTML content in sandbox-resource-ready notification.");
}
} 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);