Skip to content

Commit d310c4d

Browse files
committed
fix sandbox isolation + "test in prod"
1 parent ca428a8 commit d310c4d

File tree

6 files changed

+93
-63
lines changed

6 files changed

+93
-63
lines changed

examples/simple-host/package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
"version": "1.0.0",
55
"type": "module",
66
"scripts": {
7-
"start:server": "tsx server.ts",
7+
"start:server": "bun serve.ts",
88
"start:mcp-server": "cd ../simple-server && npm install && npm run start",
99
"build": "concurrently 'INPUT=example-host-vanilla.html vite build' 'INPUT=example-host-react.html vite build' 'INPUT=sandbox.html vite build'",
10-
"server": "bun server.ts",
1110
"start": "NODE_ENV=development npm run build && concurrently 'npm run start:server' 'npm run start:mcp-server'"
1211
},
1312
"dependencies": {
@@ -23,9 +22,9 @@
2322
"@types/react-dom": "^19.2.2",
2423
"@types/react": "^19.2.2",
2524
"@vitejs/plugin-react": "^4.3.4",
25+
"bun": "^1.3.2",
2626
"concurrently": "^9.2.1",
2727
"cors": "^2.8.5",
28-
"esbuild": "~0.19.10",
2928
"express": "^5.1.0",
3029
"prettier": "^3.6.2",
3130
"tsx": "^4.20.6",

examples/simple-host/serve.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
#!/usr/bin/env npx tsx
2+
/**
3+
* HTTP servers for the MCP UI example:
4+
* - Host server (port 8080): serves host HTML files (React and Vanilla examples)
5+
* - Sandbox server (port 8081): serves sandbox.html with permissive CSP
6+
*
7+
* Running on separate ports ensures proper origin isolation for security.
8+
*/
9+
10+
import express from "express";
11+
import cors from "cors";
12+
import { fileURLToPath } from "url";
13+
import { dirname, join } from "path";
14+
15+
const __filename = fileURLToPath(import.meta.url);
16+
const __dirname = dirname(__filename);
17+
18+
const HOST_PORT = parseInt(process.env.HOST_PORT || "8080", 10);
19+
const SANDBOX_PORT = parseInt(process.env.SANDBOX_PORT || "8081", 10);
20+
const DIRECTORY = join(__dirname, "dist");
21+
22+
// ============ Host Server (port 8080) ============
23+
const hostApp = express();
24+
hostApp.use(cors());
25+
26+
// Exclude sandbox.html from host server
27+
hostApp.use((req, res, next) => {
28+
if (req.path === "/sandbox.html") {
29+
res.status(404).send("Sandbox is served on a different port");
30+
return;
31+
}
32+
next();
33+
});
34+
35+
hostApp.use(express.static(DIRECTORY));
36+
37+
hostApp.get("/", (_req, res) => {
38+
res.redirect("/example-host-react.html");
39+
});
40+
41+
// ============ Sandbox Server (port 8081) ============
42+
const sandboxApp = express();
43+
sandboxApp.use(cors());
44+
45+
// Permissive CSP for sandbox content
46+
sandboxApp.use((_req, res, next) => {
47+
const csp = [
48+
"default-src 'self'",
49+
"img-src * data: blob: 'unsafe-inline'",
50+
"style-src * blob: data: 'unsafe-inline'",
51+
"script-src * blob: data: 'unsafe-inline' 'unsafe-eval'",
52+
"connect-src *",
53+
"font-src * blob: data:",
54+
"media-src * blob: data:",
55+
"frame-src * blob: data:",
56+
].join("; ");
57+
res.setHeader("Content-Security-Policy", csp);
58+
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
59+
res.setHeader("Pragma", "no-cache");
60+
res.setHeader("Expires", "0");
61+
next();
62+
});
63+
64+
sandboxApp.get(["/", "/sandbox.html"], (_req, res) => {
65+
res.sendFile(join(DIRECTORY, "sandbox.html"));
66+
});
67+
68+
sandboxApp.use((_req, res) => {
69+
res.status(404).send("Only sandbox.html is served on this port");
70+
});
71+
72+
// ============ Start both servers ============
73+
hostApp.listen(HOST_PORT, () => {
74+
console.log(`Host server: http://localhost:${HOST_PORT}`);
75+
});
76+
77+
sandboxApp.listen(SANDBOX_PORT, () => {
78+
console.log(`Sandbox server: http://localhost:${SANDBOX_PORT}`);
79+
console.log("\nPress Ctrl+C to stop\n");
80+
});

examples/simple-host/server.ts

Lines changed: 0 additions & 58 deletions
This file was deleted.

examples/simple-host/src/example-host-react.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Tool } from "@modelcontextprotocol/sdk/types.js";
88
import { AppRenderer, AppRendererProps } from "../src/AppRenderer";
99
import { AppBridge } from "../../../dist/src/app-bridge";
1010

11-
const SANDBOX_PROXY_URL = URL.parse("/sandbox.html", location.href)!;
11+
const SANDBOX_PROXY_URL = new URL("http://localhost:8081/sandbox.html");
1212

1313
/**
1414
* Example React application demonstrating the AppRenderer component.

examples/simple-host/src/example-host-vanilla.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
McpUiSizeChangeNotificationSchema,
1919
} from "@modelcontextprotocol/ext-apps";
2020

21-
const SANDBOX_PROXY_URL = URL.parse("/sandbox.html", location.href)!;
21+
const SANDBOX_PROXY_URL = new URL("http://localhost:8081/sandbox.html");
2222

2323
window.addEventListener("load", async () => {
2424
const client = new Client({

examples/simple-host/src/sandbox.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,15 @@ if (!document.referrer.match(/^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/)) {
1515
);
1616
}
1717

18+
// Try and break out of this iframe
19+
try {
20+
window.top!.alert("If you see this, the sandbox is not setup securely.");
21+
22+
throw new Error('Managed to break out of iframe, the sandbox is not setup securely.');
23+
} catch (e) {
24+
// Ignore
25+
}
26+
1827
const inner = document.createElement("iframe");
1928
inner.style = "width:100%; height:100%; border:none;";
2029
inner.setAttribute("sandbox", "allow-scripts allow-same-origin allow-forms");

0 commit comments

Comments
 (0)