-
Notifications
You must be signed in to change notification settings - Fork 64
fix: proper CSP handling in basic-host sandboxing (HTTP headers, worker-src, document.write) #234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
066531a
73b69e7
e072ed7
adf7088
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -62,26 +62,9 @@ const PROXY_READY_NOTIFICATION: McpUiSandboxProxyReadyNotification["method"] = | |
| // 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. | ||
| // Build CSP meta tag from domains | ||
| function buildCspMetaTag(csp?: { connectDomains?: string[]; resourceDomains?: string[] }): string { | ||
| const resourceDomains = csp?.resourceDomains?.join(" ") ?? ""; | ||
| const connectDomains = csp?.connectDomains?.join(" ") ?? ""; | ||
|
|
||
| // Base CSP directives | ||
| const directives = [ | ||
| "default-src 'self'", | ||
| `script-src 'self' 'unsafe-inline' 'unsafe-eval' blob: data: ${resourceDomains}`.trim(), | ||
| `style-src 'self' 'unsafe-inline' blob: data: ${resourceDomains}`.trim(), | ||
| `img-src 'self' data: blob: ${resourceDomains}`.trim(), | ||
| `font-src 'self' data: blob: ${resourceDomains}`.trim(), | ||
| `connect-src 'self' ${connectDomains}`.trim(), | ||
| "frame-src 'none'", | ||
| "object-src 'none'", | ||
| "base-uri 'self'", | ||
| ]; | ||
|
|
||
| return `<meta http-equiv="Content-Security-Policy" content="${directives.join("; ")}">`; | ||
| } | ||
| // | ||
| // 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) { | ||
|
|
@@ -98,29 +81,26 @@ window.addEventListener("message", async (event) => { | |
| } | ||
|
|
||
| if (event.data && event.data.method === RESOURCE_READY_NOTIFICATION) { | ||
| const { html, sandbox, csp } = event.data.params; | ||
| const { html, sandbox } = event.data.params; | ||
| if (typeof sandbox === "string") { | ||
| inner.setAttribute("sandbox", sandbox); | ||
| } | ||
| if (typeof html === "string") { | ||
| // Inject CSP meta tag at the start of <head> if CSP is provided | ||
| console.log("[Sandbox] Received CSP:", csp); | ||
| let modifiedHtml = html; | ||
| if (csp) { | ||
| const cspMetaTag = buildCspMetaTag(csp); | ||
| console.log("[Sandbox] Injecting CSP meta tag:", cspMetaTag); | ||
| // Insert after <head> tag if present, otherwise prepend | ||
| if (modifiedHtml.includes("<head>")) { | ||
| modifiedHtml = modifiedHtml.replace("<head>", `<head>\n${cspMetaTag}`); | ||
| } else if (modifiedHtml.includes("<head ")) { | ||
| modifiedHtml = modifiedHtml.replace(/<head[^>]*>/, `$&\n${cspMetaTag}`); | ||
| } else { | ||
| modifiedHtml = cspMetaTag + modifiedHtml; | ||
| } | ||
| // Use document.write instead of srcdoc for WebGL compatibility. | ||
| // srcdoc creates an opaque origin which prevents WebGL canvas updates | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm confused by this bit. srcdoc iframes are not all opaque-origined. For example, navigate to https://example.com and run this in DevTools: const iframe = document.createElement('iframe');
iframe.srcdoc = `<p id=log></p><script>log.textContent = self.origin</script>`;
document.body.append(iframe);By default, srcdoc iframes inherit their navigation initiator's origin (i.e., the origin of the document that set the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oops sorry good catch, will send update Real reason: was to work around browser canvas tainting checks that seemed to treat about:srcdoc URLs as tainted, even when the iframe is same-origin with its parent. Will confirm and update the comment. in a followup |
||
| // 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 { | ||
| console.log("[Sandbox] No CSP provided, using default"); | ||
| // Fallback to srcdoc if document is not accessible | ||
| console.warn("[Sandbox] document.write not available, falling back to srcdoc"); | ||
| inner.srcdoc = html; | ||
| } | ||
| inner.srcdoc = modifiedHtml; | ||
| } | ||
| } else { | ||
| if (inner && inner.contentWindow) { | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@claude do I understand correctly that we have a risk of csp injection here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pushed a fix, ptal @antonpk1