@modelcontextprotocol/inspector 1.0.1 → 2.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +199 -361
- package/clients/cli/build/index.js +13218 -0
- package/clients/launcher/build/index.js +90 -0
- package/clients/launcher/build/parse-launcher-argv.js +29 -0
- package/clients/tui/build/chunk-MCKGQKYU.js +15 -0
- package/clients/tui/build/index.js +14573 -0
- package/clients/tui/build/undici-HXPKCIY3.js +25789 -0
- package/clients/web/build/chunk-MCKGQKYU.js +15 -0
- package/clients/web/build/index.js +4322 -0
- package/clients/web/build/undici-HXPKCIY3.js +25789 -0
- package/clients/web/dist/assets/css-DfBGKsUM.js +1 -0
- package/clients/web/dist/assets/index-Cbtmmw0V.js +300 -0
- package/clients/web/dist/assets/index-CiUpWldH.css +1 -0
- package/clients/web/dist/assets/json-rfaH9Mx5.js +1 -0
- package/clients/web/dist/assets/markdown-PVXc4EJ0.js +1 -0
- package/clients/web/dist/assets/markup-Bk4keGDK.js +1 -0
- package/clients/web/dist/assets/markup-DPsXihXF.js +1 -0
- package/clients/web/dist/assets/prism-light-B8tIwqDr.js +6 -0
- package/clients/web/dist/assets/tomorrow-COrD2Qnh.js +1 -0
- package/clients/web/dist/assets/yaml-B78V9E0m.js +1 -0
- package/clients/web/dist/icons.svg +24 -0
- package/clients/web/dist/index.html +17 -0
- package/package.json +96 -80
- package/scripts/install-clients.mjs +65 -0
- package/LICENSE +0 -216
- package/cli/build/cli.js +0 -290
- package/cli/build/client/connection.js +0 -38
- package/cli/build/client/index.js +0 -6
- package/cli/build/client/prompts.js +0 -43
- package/cli/build/client/resources.js +0 -36
- package/cli/build/client/tools.js +0 -87
- package/cli/build/client/types.js +0 -1
- package/cli/build/error-handler.js +0 -18
- package/cli/build/index.js +0 -257
- package/cli/build/transport.js +0 -65
- package/cli/build/utils/awaitable-log.js +0 -7
- package/client/bin/client.js +0 -62
- package/client/bin/start.js +0 -355
- package/client/dist/assets/OAuthCallback-DKaW5Tk8.js +0 -55
- package/client/dist/assets/OAuthDebugCallback-Bx3X7Zkh.js +0 -64
- package/client/dist/assets/index-CH-qD91V.js +0 -67487
- package/client/dist/assets/index-DXy0twoH.css +0 -2786
- package/client/dist/index.html +0 -14
- package/server/build/index.js +0 -889
- package/server/build/mcpProxy.js +0 -121
- package/server/build/static/sandbox_proxy.html +0 -197
- package/server/static/sandbox_proxy.html +0 -197
- /package/{client/dist/mcp.svg → clients/web/dist/assets/MCP-CWGQJYyH.svg} +0 -0
package/server/build/mcpProxy.js
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import { SseError } from "@modelcontextprotocol/sdk/client/sse.js";
|
|
2
|
-
import { StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
3
|
-
import { isJSONRPCRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
4
|
-
/**
|
|
5
|
-
* JSON-RPC error code for failed proxy → upstream transport sends.
|
|
6
|
-
*
|
|
7
|
-
* JSON-RPC 2.0 reserves **-32000 .. -32099** for implementation-defined *server* errors.
|
|
8
|
-
* MCP uses that band for protocol/SDK errors (see `@modelcontextprotocol/sdk` `ErrorCode`, e.g.
|
|
9
|
-
* `-32000` connection closed, `-32001` request timeout, `-32042` URL elicitation; other codes in
|
|
10
|
-
* the band are assigned over time — e.g. **-32002** is used for **ResourceNotFound** in parts of
|
|
11
|
-
* the MCP ecosystem). We use **-32099** so this inspector-only bridge error stays clear of those
|
|
12
|
-
* registered meanings; the client still keys off `error.data` shape, not the number alone.
|
|
13
|
-
*
|
|
14
|
-
* Keep in sync with `client/src/lib/constants.ts`; drift is caught by
|
|
15
|
-
* `client/src/lib/__tests__/mcpProxyTransportErrorCode.test.ts`.
|
|
16
|
-
*/
|
|
17
|
-
export const MCP_PROXY_TRANSPORT_ERROR_CODE = -32099;
|
|
18
|
-
function onClientError(error) {
|
|
19
|
-
console.error("Error from inspector client:", error);
|
|
20
|
-
}
|
|
21
|
-
function onServerError(error) {
|
|
22
|
-
if (error?.cause && JSON.stringify(error.cause).includes("ECONNREFUSED")) {
|
|
23
|
-
console.error("Connection refused. Is the MCP server running?");
|
|
24
|
-
}
|
|
25
|
-
else if (error.message && error.message.includes("404")) {
|
|
26
|
-
console.error("Error accessing endpoint (HTTP 404)");
|
|
27
|
-
}
|
|
28
|
-
else {
|
|
29
|
-
console.error("Error from MCP server:", error);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
/** Exported for unit tests; used by the proxy when forwarding transport failures. */
|
|
33
|
-
export function serializeProxyTransportError(error, headerHolder) {
|
|
34
|
-
const data = {
|
|
35
|
-
message: error.message,
|
|
36
|
-
name: error.name,
|
|
37
|
-
};
|
|
38
|
-
if (error.cause !== undefined) {
|
|
39
|
-
data.cause =
|
|
40
|
-
error.cause instanceof Error ? error.cause.message : String(error.cause);
|
|
41
|
-
}
|
|
42
|
-
const attachHttpStatusIfValid = (code) => {
|
|
43
|
-
if (Number.isInteger(code) && code >= 100 && code <= 599) {
|
|
44
|
-
data.httpStatus = code;
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
if (error instanceof StreamableHTTPError) {
|
|
48
|
-
if (error.code !== undefined)
|
|
49
|
-
attachHttpStatusIfValid(error.code);
|
|
50
|
-
}
|
|
51
|
-
else if (error instanceof SseError) {
|
|
52
|
-
if (error.code !== undefined)
|
|
53
|
-
attachHttpStatusIfValid(error.code);
|
|
54
|
-
}
|
|
55
|
-
if (headerHolder?.lastUpstream401) {
|
|
56
|
-
const u = headerHolder.lastUpstream401;
|
|
57
|
-
data.upstream401 = {
|
|
58
|
-
wwwAuthenticate: u.wwwAuthenticate,
|
|
59
|
-
body: u.body,
|
|
60
|
-
contentType: u.contentType,
|
|
61
|
-
};
|
|
62
|
-
delete headerHolder.lastUpstream401;
|
|
63
|
-
}
|
|
64
|
-
return data;
|
|
65
|
-
}
|
|
66
|
-
export default function mcpProxy({ transportToClient, transportToServer, headerHolder, }) {
|
|
67
|
-
let transportToClientClosed = false;
|
|
68
|
-
let transportToServerClosed = false;
|
|
69
|
-
let reportedServerSession = false;
|
|
70
|
-
transportToClient.onmessage = (message) => {
|
|
71
|
-
transportToServer.send(message).catch((err) => {
|
|
72
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
73
|
-
// Send error response back to client if it was a request (has id) and connection is still open
|
|
74
|
-
if (isJSONRPCRequest(message) && !transportToClientClosed) {
|
|
75
|
-
const causeStr = error.cause !== undefined
|
|
76
|
-
? error.cause instanceof Error
|
|
77
|
-
? error.cause.message
|
|
78
|
-
: String(error.cause)
|
|
79
|
-
: null;
|
|
80
|
-
const errorResponse = {
|
|
81
|
-
jsonrpc: "2.0",
|
|
82
|
-
id: message.id,
|
|
83
|
-
error: {
|
|
84
|
-
code: MCP_PROXY_TRANSPORT_ERROR_CODE,
|
|
85
|
-
message: causeStr
|
|
86
|
-
? `${error.message} (cause: ${causeStr})`
|
|
87
|
-
: error.message,
|
|
88
|
-
data: serializeProxyTransportError(error, headerHolder),
|
|
89
|
-
},
|
|
90
|
-
};
|
|
91
|
-
transportToClient.send(errorResponse).catch(onClientError);
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
};
|
|
95
|
-
transportToServer.onmessage = (message) => {
|
|
96
|
-
if (!reportedServerSession) {
|
|
97
|
-
if (transportToServer.sessionId) {
|
|
98
|
-
// Can only report for StreamableHttp
|
|
99
|
-
console.error("Proxy <-> Server sessionId: " + transportToServer.sessionId);
|
|
100
|
-
}
|
|
101
|
-
reportedServerSession = true;
|
|
102
|
-
}
|
|
103
|
-
transportToClient.send(message).catch(onClientError);
|
|
104
|
-
};
|
|
105
|
-
transportToClient.onclose = () => {
|
|
106
|
-
if (transportToServerClosed) {
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
transportToClientClosed = true;
|
|
110
|
-
transportToServer.close().catch(onServerError);
|
|
111
|
-
};
|
|
112
|
-
transportToServer.onclose = () => {
|
|
113
|
-
if (transportToClientClosed) {
|
|
114
|
-
return;
|
|
115
|
-
}
|
|
116
|
-
transportToServerClosed = true;
|
|
117
|
-
transportToClient.close().catch(onClientError);
|
|
118
|
-
};
|
|
119
|
-
transportToClient.onerror = onClientError;
|
|
120
|
-
transportToServer.onerror = onServerError;
|
|
121
|
-
}
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta name="color-scheme" content="light dark" />
|
|
6
|
-
<!-- CSP is set via HTTP headers by serve.ts (based on ?csp= query param).
|
|
7
|
-
The inner iframe inherits this CSP since we use document.write(). -->
|
|
8
|
-
<title>MCP-UI Proxy</title>
|
|
9
|
-
<style>
|
|
10
|
-
html,
|
|
11
|
-
body {
|
|
12
|
-
margin: 0;
|
|
13
|
-
height: 100vh;
|
|
14
|
-
width: 100vw;
|
|
15
|
-
/* Transparent background allows parent page to show through */
|
|
16
|
-
background-color: transparent;
|
|
17
|
-
}
|
|
18
|
-
body {
|
|
19
|
-
display: flex;
|
|
20
|
-
flex-direction: column;
|
|
21
|
-
}
|
|
22
|
-
* {
|
|
23
|
-
box-sizing: border-box;
|
|
24
|
-
}
|
|
25
|
-
iframe {
|
|
26
|
-
background-color: transparent;
|
|
27
|
-
border: 0px none transparent;
|
|
28
|
-
padding: 0px;
|
|
29
|
-
overflow: hidden;
|
|
30
|
-
flex-grow: 1;
|
|
31
|
-
/* Inherit color scheme from parent for consistent transparency */
|
|
32
|
-
color-scheme: inherit;
|
|
33
|
-
}
|
|
34
|
-
</style>
|
|
35
|
-
</head>
|
|
36
|
-
<body>
|
|
37
|
-
<script>
|
|
38
|
-
function buildAllowAttribute(permissions) {
|
|
39
|
-
if (!permissions) return "";
|
|
40
|
-
|
|
41
|
-
const allowList = [];
|
|
42
|
-
if (permissions["camera"]) allowList.push("camera");
|
|
43
|
-
if (permissions["microphone"]) allowList.push("microphone");
|
|
44
|
-
if (permissions["geolocation"]) allowList.push("geolocation");
|
|
45
|
-
if (permissions["clipboardWrite"]) allowList.push("clipboard-write");
|
|
46
|
-
|
|
47
|
-
return allowList.join("; ");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const ALLOWED_REFERRER_PATTERN =
|
|
51
|
-
/^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/;
|
|
52
|
-
|
|
53
|
-
if (window.self === window.top) {
|
|
54
|
-
throw new Error("This file is only to be used in an iframe sandbox.");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (!document.referrer) {
|
|
58
|
-
throw new Error("No referrer, cannot validate embedding site.");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!document.referrer.match(ALLOWED_REFERRER_PATTERN)) {
|
|
62
|
-
throw new Error(
|
|
63
|
-
`Embedding domain not allowed in referrer ${document.referrer}. (Consider updating the validation logic to allow your domain.)`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Extract the expected host origin from the referrer for origin validation.
|
|
68
|
-
// This is the origin we expect all parent messages to come from.
|
|
69
|
-
const EXPECTED_HOST_ORIGIN = new URL(document.referrer).origin;
|
|
70
|
-
|
|
71
|
-
const OWN_ORIGIN = new URL(window.location.href).origin;
|
|
72
|
-
|
|
73
|
-
// Security self-test: verify iframe isolation is working correctly.
|
|
74
|
-
// This MUST throw a SecurityError -- if `window.top` is accessible, the sandbox
|
|
75
|
-
// configuration is dangerously broken and untrusted content could escape.
|
|
76
|
-
try {
|
|
77
|
-
window.top?.alert(
|
|
78
|
-
"If you see this, the sandbox is not setup securely.",
|
|
79
|
-
);
|
|
80
|
-
throw "FAIL";
|
|
81
|
-
} catch (e) {
|
|
82
|
-
if (e === "FAIL") {
|
|
83
|
-
throw new Error("The sandbox is not setup securely.");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Expected: SecurityError confirms proper sandboxing.
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Double-iframe sandbox architecture: THIS file is the outer sandbox proxy
|
|
90
|
-
// iframe on a separate origin. It creates an inner iframe for untrusted HTML
|
|
91
|
-
// content. Per the specification, the Host and the Sandbox MUST have different
|
|
92
|
-
// origins.
|
|
93
|
-
const inner = document.createElement("iframe");
|
|
94
|
-
inner.style = "width:100%; height:100%; border:none;";
|
|
95
|
-
inner.setAttribute(
|
|
96
|
-
"sandbox",
|
|
97
|
-
"allow-scripts allow-same-origin allow-forms",
|
|
98
|
-
);
|
|
99
|
-
// Note: allow attribute is set later when receiving sandbox-resource-ready notification
|
|
100
|
-
// based on the permissions requested by the app
|
|
101
|
-
document.body.appendChild(inner);
|
|
102
|
-
|
|
103
|
-
const RESOURCE_READY_NOTIFICATION =
|
|
104
|
-
"ui/notifications/sandbox-resource-ready";
|
|
105
|
-
const PROXY_READY_NOTIFICATION = "ui/notifications/sandbox-proxy-ready";
|
|
106
|
-
|
|
107
|
-
// Message relay: This Sandbox (outer iframe) acts as a bidirectional bridge,
|
|
108
|
-
// forwarding messages between:
|
|
109
|
-
//
|
|
110
|
-
// Host (parent window) ↔ Sandbox (outer frame) ↔ View (inner iframe)
|
|
111
|
-
//
|
|
112
|
-
// Reason: the parent window and inner iframe have different origins and can't
|
|
113
|
-
// communicate directly, so the outer iframe forwards messages in both
|
|
114
|
-
// directions to connect them.
|
|
115
|
-
//
|
|
116
|
-
// Special case: The "ui/notifications/sandbox-proxy-ready" message is
|
|
117
|
-
// intercepted here (not relayed) because the Sandbox uses it to configure and
|
|
118
|
-
// load the inner iframe with the view HTML content.
|
|
119
|
-
//
|
|
120
|
-
// Security: CSP is enforced via HTTP headers on sandbox.html (set by serve.ts
|
|
121
|
-
// based on ?csp= query param). This is tamper-proof unlike meta tags.
|
|
122
|
-
|
|
123
|
-
window.addEventListener("message", async (event) => {
|
|
124
|
-
if (event.source === window.parent) {
|
|
125
|
-
// Validate that messages from parent come from the expected host origin.
|
|
126
|
-
// This prevents malicious pages from sending messages to this sandbox.
|
|
127
|
-
if (event.origin !== EXPECTED_HOST_ORIGIN) {
|
|
128
|
-
console.error(
|
|
129
|
-
"[Sandbox] Rejecting message from unexpected origin:",
|
|
130
|
-
event.origin,
|
|
131
|
-
"expected:",
|
|
132
|
-
EXPECTED_HOST_ORIGIN,
|
|
133
|
-
);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (event.data && event.data.method === RESOURCE_READY_NOTIFICATION) {
|
|
138
|
-
const { html, sandbox, permissions } = event.data.params;
|
|
139
|
-
if (typeof sandbox === "string") {
|
|
140
|
-
inner.setAttribute("sandbox", sandbox);
|
|
141
|
-
}
|
|
142
|
-
// Set Permission Policy allow attribute if permissions are requested
|
|
143
|
-
const allowAttribute = buildAllowAttribute(permissions);
|
|
144
|
-
if (allowAttribute) {
|
|
145
|
-
console.log("[Sandbox] Setting allow attribute:", allowAttribute);
|
|
146
|
-
inner.setAttribute("allow", allowAttribute);
|
|
147
|
-
}
|
|
148
|
-
if (typeof html === "string") {
|
|
149
|
-
// Use document.write instead of srcdoc (which the CesiumJS Map won't work with)
|
|
150
|
-
const doc =
|
|
151
|
-
inner.contentDocument || inner.contentWindow?.document;
|
|
152
|
-
if (doc) {
|
|
153
|
-
doc.open();
|
|
154
|
-
doc.write(html);
|
|
155
|
-
doc.close();
|
|
156
|
-
} else {
|
|
157
|
-
// Fallback to srcdoc if document is not accessible
|
|
158
|
-
console.warn(
|
|
159
|
-
"[Sandbox] document.write not available, falling back to srcdoc",
|
|
160
|
-
);
|
|
161
|
-
inner.srcdoc = html;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
} else {
|
|
165
|
-
if (inner && inner.contentWindow) {
|
|
166
|
-
inner.contentWindow.postMessage(event.data, "*");
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
} else if (event.source === inner.contentWindow) {
|
|
170
|
-
if (event.origin !== OWN_ORIGIN) {
|
|
171
|
-
console.error(
|
|
172
|
-
"[Sandbox] Rejecting message from inner iframe with unexpected origin:",
|
|
173
|
-
event.origin,
|
|
174
|
-
"expected:",
|
|
175
|
-
OWN_ORIGIN,
|
|
176
|
-
);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
// Relay messages from inner frame to parent window.
|
|
180
|
-
// Use specific origin instead of "*" to prevent message interception.
|
|
181
|
-
window.parent.postMessage(event.data, EXPECTED_HOST_ORIGIN);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// Notify the Host that the Sandbox is ready to receive view HTML.
|
|
186
|
-
// Use specific origin instead of "*" to ensure only the expected host receives this.
|
|
187
|
-
window.parent.postMessage(
|
|
188
|
-
{
|
|
189
|
-
jsonrpc: "2.0",
|
|
190
|
-
method: PROXY_READY_NOTIFICATION,
|
|
191
|
-
params: {},
|
|
192
|
-
},
|
|
193
|
-
EXPECTED_HOST_ORIGIN,
|
|
194
|
-
);
|
|
195
|
-
</script>
|
|
196
|
-
</body>
|
|
197
|
-
</html>
|
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
<!doctype html>
|
|
2
|
-
<html>
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="utf-8" />
|
|
5
|
-
<meta name="color-scheme" content="light dark" />
|
|
6
|
-
<!-- CSP is set via HTTP headers by serve.ts (based on ?csp= query param).
|
|
7
|
-
The inner iframe inherits this CSP since we use document.write(). -->
|
|
8
|
-
<title>MCP-UI Proxy</title>
|
|
9
|
-
<style>
|
|
10
|
-
html,
|
|
11
|
-
body {
|
|
12
|
-
margin: 0;
|
|
13
|
-
height: 100vh;
|
|
14
|
-
width: 100vw;
|
|
15
|
-
/* Transparent background allows parent page to show through */
|
|
16
|
-
background-color: transparent;
|
|
17
|
-
}
|
|
18
|
-
body {
|
|
19
|
-
display: flex;
|
|
20
|
-
flex-direction: column;
|
|
21
|
-
}
|
|
22
|
-
* {
|
|
23
|
-
box-sizing: border-box;
|
|
24
|
-
}
|
|
25
|
-
iframe {
|
|
26
|
-
background-color: transparent;
|
|
27
|
-
border: 0px none transparent;
|
|
28
|
-
padding: 0px;
|
|
29
|
-
overflow: hidden;
|
|
30
|
-
flex-grow: 1;
|
|
31
|
-
/* Inherit color scheme from parent for consistent transparency */
|
|
32
|
-
color-scheme: inherit;
|
|
33
|
-
}
|
|
34
|
-
</style>
|
|
35
|
-
</head>
|
|
36
|
-
<body>
|
|
37
|
-
<script>
|
|
38
|
-
function buildAllowAttribute(permissions) {
|
|
39
|
-
if (!permissions) return "";
|
|
40
|
-
|
|
41
|
-
const allowList = [];
|
|
42
|
-
if (permissions["camera"]) allowList.push("camera");
|
|
43
|
-
if (permissions["microphone"]) allowList.push("microphone");
|
|
44
|
-
if (permissions["geolocation"]) allowList.push("geolocation");
|
|
45
|
-
if (permissions["clipboardWrite"]) allowList.push("clipboard-write");
|
|
46
|
-
|
|
47
|
-
return allowList.join("; ");
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const ALLOWED_REFERRER_PATTERN =
|
|
51
|
-
/^http:\/\/(localhost|127\.0\.0\.1)(:|\/|$)/;
|
|
52
|
-
|
|
53
|
-
if (window.self === window.top) {
|
|
54
|
-
throw new Error("This file is only to be used in an iframe sandbox.");
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (!document.referrer) {
|
|
58
|
-
throw new Error("No referrer, cannot validate embedding site.");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (!document.referrer.match(ALLOWED_REFERRER_PATTERN)) {
|
|
62
|
-
throw new Error(
|
|
63
|
-
`Embedding domain not allowed in referrer ${document.referrer}. (Consider updating the validation logic to allow your domain.)`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
// Extract the expected host origin from the referrer for origin validation.
|
|
68
|
-
// This is the origin we expect all parent messages to come from.
|
|
69
|
-
const EXPECTED_HOST_ORIGIN = new URL(document.referrer).origin;
|
|
70
|
-
|
|
71
|
-
const OWN_ORIGIN = new URL(window.location.href).origin;
|
|
72
|
-
|
|
73
|
-
// Security self-test: verify iframe isolation is working correctly.
|
|
74
|
-
// This MUST throw a SecurityError -- if `window.top` is accessible, the sandbox
|
|
75
|
-
// configuration is dangerously broken and untrusted content could escape.
|
|
76
|
-
try {
|
|
77
|
-
window.top?.alert(
|
|
78
|
-
"If you see this, the sandbox is not setup securely.",
|
|
79
|
-
);
|
|
80
|
-
throw "FAIL";
|
|
81
|
-
} catch (e) {
|
|
82
|
-
if (e === "FAIL") {
|
|
83
|
-
throw new Error("The sandbox is not setup securely.");
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Expected: SecurityError confirms proper sandboxing.
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Double-iframe sandbox architecture: THIS file is the outer sandbox proxy
|
|
90
|
-
// iframe on a separate origin. It creates an inner iframe for untrusted HTML
|
|
91
|
-
// content. Per the specification, the Host and the Sandbox MUST have different
|
|
92
|
-
// origins.
|
|
93
|
-
const inner = document.createElement("iframe");
|
|
94
|
-
inner.style = "width:100%; height:100%; border:none;";
|
|
95
|
-
inner.setAttribute(
|
|
96
|
-
"sandbox",
|
|
97
|
-
"allow-scripts allow-same-origin allow-forms",
|
|
98
|
-
);
|
|
99
|
-
// Note: allow attribute is set later when receiving sandbox-resource-ready notification
|
|
100
|
-
// based on the permissions requested by the app
|
|
101
|
-
document.body.appendChild(inner);
|
|
102
|
-
|
|
103
|
-
const RESOURCE_READY_NOTIFICATION =
|
|
104
|
-
"ui/notifications/sandbox-resource-ready";
|
|
105
|
-
const PROXY_READY_NOTIFICATION = "ui/notifications/sandbox-proxy-ready";
|
|
106
|
-
|
|
107
|
-
// Message relay: This Sandbox (outer iframe) acts as a bidirectional bridge,
|
|
108
|
-
// forwarding messages between:
|
|
109
|
-
//
|
|
110
|
-
// Host (parent window) ↔ Sandbox (outer frame) ↔ View (inner iframe)
|
|
111
|
-
//
|
|
112
|
-
// Reason: the parent window and inner iframe have different origins and can't
|
|
113
|
-
// communicate directly, so the outer iframe forwards messages in both
|
|
114
|
-
// directions to connect them.
|
|
115
|
-
//
|
|
116
|
-
// Special case: The "ui/notifications/sandbox-proxy-ready" message is
|
|
117
|
-
// intercepted here (not relayed) because the Sandbox uses it to configure and
|
|
118
|
-
// load the inner iframe with the view HTML content.
|
|
119
|
-
//
|
|
120
|
-
// Security: CSP is enforced via HTTP headers on sandbox.html (set by serve.ts
|
|
121
|
-
// based on ?csp= query param). This is tamper-proof unlike meta tags.
|
|
122
|
-
|
|
123
|
-
window.addEventListener("message", async (event) => {
|
|
124
|
-
if (event.source === window.parent) {
|
|
125
|
-
// Validate that messages from parent come from the expected host origin.
|
|
126
|
-
// This prevents malicious pages from sending messages to this sandbox.
|
|
127
|
-
if (event.origin !== EXPECTED_HOST_ORIGIN) {
|
|
128
|
-
console.error(
|
|
129
|
-
"[Sandbox] Rejecting message from unexpected origin:",
|
|
130
|
-
event.origin,
|
|
131
|
-
"expected:",
|
|
132
|
-
EXPECTED_HOST_ORIGIN,
|
|
133
|
-
);
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (event.data && event.data.method === RESOURCE_READY_NOTIFICATION) {
|
|
138
|
-
const { html, sandbox, permissions } = event.data.params;
|
|
139
|
-
if (typeof sandbox === "string") {
|
|
140
|
-
inner.setAttribute("sandbox", sandbox);
|
|
141
|
-
}
|
|
142
|
-
// Set Permission Policy allow attribute if permissions are requested
|
|
143
|
-
const allowAttribute = buildAllowAttribute(permissions);
|
|
144
|
-
if (allowAttribute) {
|
|
145
|
-
console.log("[Sandbox] Setting allow attribute:", allowAttribute);
|
|
146
|
-
inner.setAttribute("allow", allowAttribute);
|
|
147
|
-
}
|
|
148
|
-
if (typeof html === "string") {
|
|
149
|
-
// Use document.write instead of srcdoc (which the CesiumJS Map won't work with)
|
|
150
|
-
const doc =
|
|
151
|
-
inner.contentDocument || inner.contentWindow?.document;
|
|
152
|
-
if (doc) {
|
|
153
|
-
doc.open();
|
|
154
|
-
doc.write(html);
|
|
155
|
-
doc.close();
|
|
156
|
-
} else {
|
|
157
|
-
// Fallback to srcdoc if document is not accessible
|
|
158
|
-
console.warn(
|
|
159
|
-
"[Sandbox] document.write not available, falling back to srcdoc",
|
|
160
|
-
);
|
|
161
|
-
inner.srcdoc = html;
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
} else {
|
|
165
|
-
if (inner && inner.contentWindow) {
|
|
166
|
-
inner.contentWindow.postMessage(event.data, "*");
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
} else if (event.source === inner.contentWindow) {
|
|
170
|
-
if (event.origin !== OWN_ORIGIN) {
|
|
171
|
-
console.error(
|
|
172
|
-
"[Sandbox] Rejecting message from inner iframe with unexpected origin:",
|
|
173
|
-
event.origin,
|
|
174
|
-
"expected:",
|
|
175
|
-
OWN_ORIGIN,
|
|
176
|
-
);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
// Relay messages from inner frame to parent window.
|
|
180
|
-
// Use specific origin instead of "*" to prevent message interception.
|
|
181
|
-
window.parent.postMessage(event.data, EXPECTED_HOST_ORIGIN);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
// Notify the Host that the Sandbox is ready to receive view HTML.
|
|
186
|
-
// Use specific origin instead of "*" to ensure only the expected host receives this.
|
|
187
|
-
window.parent.postMessage(
|
|
188
|
-
{
|
|
189
|
-
jsonrpc: "2.0",
|
|
190
|
-
method: PROXY_READY_NOTIFICATION,
|
|
191
|
-
params: {},
|
|
192
|
-
},
|
|
193
|
-
EXPECTED_HOST_ORIGIN,
|
|
194
|
-
);
|
|
195
|
-
</script>
|
|
196
|
-
</body>
|
|
197
|
-
</html>
|
|
File without changes
|