example-host: fix sandboxing setup (#21)

* example-host: prevent accidental non-iframing of sandbox (or iframing in prod)

* fix sandbox isolation + "test in prod"

* Update package.json

* Update sandbox.ts
This commit is contained in:
Olivier Chafik 2025-11-25 02:45:55 +00:00 • committed by GitHub
commit b4c6019d47
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 116 additions and 66 deletions

View file

@ -8,7 +8,7 @@ import { Tool } from "@modelcontextprotocol/sdk/types.js";
import { AppRenderer, AppRendererProps } from "../src/AppRenderer";
import { AppBridge } from "../../../dist/src/app-bridge";
const SANDBOX_PROXY_URL = URL.parse("/sandbox.html", location.href)!;
const SANDBOX_PROXY_URL = new URL("http://localhost:8081/sandbox.html");
/**
* Example React application demonstrating the AppRenderer component.

View file

@ -18,7 +18,7 @@ import {
McpUiSizeChangeNotificationSchema,
} from "@modelcontextprotocol/ext-apps";
const SANDBOX_PROXY_URL = URL.parse("/sandbox.html", location.href)!;
const SANDBOX_PROXY_URL = new URL("http://localhost:8081/sandbox.html");
window.addEventListener("load", async () => {
const client = new Client({

View file

@ -1,16 +1,43 @@
// Double-iframe raw HTML mode (HTML sent via postMessage)
import type {
McpUiSandboxProxyReadyNotification,
McpUiSandboxResourceReadyNotification,
} from "../../../dist/src/types";
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(/^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/)) {
throw new Error(
`Embedding domain not allowed in referrer ${document.referrer} (update the validation logic to allow your domain)`,
);
}
// Try and break out of this iframe
try {
window.top!.alert("If you see this, the sandbox is not setup securely.");
throw new Error(
"Managed to break out of iframe, the sandbox is not setup securely.",
);
} catch (e) {
// Ignore
}
const inner = document.createElement("iframe");
inner.style = "width:100%; height:100%; border:none;";
// sandbox will be set from postMessage payload; default minimal before html arrives
inner.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");
document.body.appendChild(inner);
// Wait for HTML content from parent
window.addEventListener("message", async (event) => {
// Note: in production you'll also want to validate event.origin against your outer domain.
if (event.source === window.parent) {
if (
event.data &&
event.data.method === "ui/notifications/sandbox-resource-ready"
event.data.method ===
("ui/notifications/sandbox-resource-ready" as McpUiSandboxResourceReadyNotification["method"])
) {
const { html, sandbox } = event.data.params;
if (typeof sandbox === "string") {
@ -34,7 +61,8 @@ window.addEventListener("message", async (event) => {
window.parent.postMessage(
{
jsonrpc: "2.0",
method: "ui/notifications/sandbox-proxy-ready",
method:
"ui/notifications/sandbox-proxy-ready" as McpUiSandboxProxyReadyNotification["method"],
params: {},
},
"*",