chore(mcp): loosen csp safety so threejs mcp apps official demo server can run

This commit is contained in:
Dustin Healy 2026-06-25 22:56:54 -07:00
parent 20afb27961
commit 2f650687d6

View file

@ -1,191 +1,218 @@
<!DOCTYPE html>
<html style="height:100%;margin:0">
<head>
<meta charset="utf-8">
<title>MCP App Sandbox</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: transparent; }
iframe { width: 100%; height: 100%; border: none; display: block; background: transparent; }
</style>
</head>
<body>
<script>
'use strict';
<!doctype html>
<html style="height: 100%; margin: 0">
<head>
<meta charset="utf-8" />
<title>MCP App Sandbox</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html,
body {
width: 100%;
height: 100%;
overflow: hidden;
background: transparent;
}
iframe {
width: 100%;
height: 100%;
border: none;
display: block;
background: transparent;
}
</style>
</head>
<body>
<script>
'use strict';
let innerFrame = null;
let innerFrameBlobUrl = null;
let readyInterval = null;
const SANDBOX_PREFIX = 'ui/notifications/sandbox-';
let innerFrame = null;
let innerFrameBlobUrl = null;
let readyInterval = null;
const SANDBOX_PREFIX = 'ui/notifications/sandbox-';
// Default to same-origin. When the sandbox is served from a dedicated origin, the parent
// passes its origin via the parentOrigin query param so the handshake targets LibreChat
// rather than the sandbox's own origin.
const trustedOrigin = (function () {
try {
const param = new URLSearchParams(window.location.search).get('parentOrigin');
if (param) {
return new URL(param).origin;
}
} catch (e) {}
return window.location.origin;
})();
function notifyReady() {
window.parent.postMessage(
{ jsonrpc: '2.0', method: 'ui/notifications/sandbox-proxy-ready', params: {} },
trustedOrigin
);
if (!readyInterval) {
readyInterval = setInterval(() => {
if (!innerFrame) {
window.parent.postMessage(
{ jsonrpc: '2.0', method: 'ui/notifications/sandbox-proxy-ready', params: {} },
trustedOrigin
);
// Default to same-origin. When the sandbox is served from a dedicated origin, the parent
// passes its origin via the parentOrigin query param so the handshake targets LibreChat
// rather than the sandbox's own origin.
const trustedOrigin = (function () {
try {
const param = new URLSearchParams(window.location.search).get('parentOrigin');
if (param) {
return new URL(param).origin;
}
}, 500);
}
}
} catch (e) {}
return window.location.origin;
})();
window.addEventListener('message', (event) => {
let msg;
try {
msg = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
} catch {
return;
function notifyReady() {
window.parent.postMessage(
{ jsonrpc: '2.0', method: 'ui/notifications/sandbox-proxy-ready', params: {} },
trustedOrigin,
);
if (!readyInterval) {
readyInterval = setInterval(() => {
if (!innerFrame) {
window.parent.postMessage(
{ jsonrpc: '2.0', method: 'ui/notifications/sandbox-proxy-ready', params: {} },
trustedOrigin,
);
}
}, 500);
}
}
if (!msg || msg.jsonrpc !== '2.0') return;
if (event.source === window.parent) {
if (event.origin !== trustedOrigin) {
window.addEventListener('message', (event) => {
let msg;
try {
msg = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
} catch {
return;
}
if (!msg || msg.jsonrpc !== '2.0') return;
if (event.source === window.parent) {
if (event.origin !== trustedOrigin) {
return;
}
if (msg.method === 'ui/notifications/sandbox-resource-ready') {
clearInterval(readyInterval);
readyInterval = null;
createInnerFrame(msg.params);
return;
}
if (innerFrame && innerFrame.contentWindow) {
innerFrame.contentWindow.postMessage(msg, '*');
}
return;
}
if (msg.method === 'ui/notifications/sandbox-resource-ready') {
clearInterval(readyInterval);
readyInterval = null;
createInnerFrame(msg.params);
return;
if (innerFrame && event.source === innerFrame.contentWindow) {
if (msg.method && msg.method.startsWith(SANDBOX_PREFIX)) {
return;
}
window.parent.postMessage(msg, trustedOrigin);
}
if (innerFrame && innerFrame.contentWindow) {
innerFrame.contentWindow.postMessage(msg, '*');
});
function createInnerFrame(params) {
const { html, csp, permissions } = params;
if (innerFrameBlobUrl) {
URL.revokeObjectURL(innerFrameBlobUrl);
innerFrameBlobUrl = null;
}
return;
}
if (innerFrame && event.source === innerFrame.contentWindow) {
if (msg.method && msg.method.startsWith(SANDBOX_PREFIX)) {
return;
if (innerFrame) {
innerFrame.remove();
}
window.parent.postMessage(msg, trustedOrigin);
}
});
function createInnerFrame(params) {
const { html, csp, permissions } = params;
innerFrame = document.createElement('iframe');
if (innerFrameBlobUrl) {
URL.revokeObjectURL(innerFrameBlobUrl);
innerFrameBlobUrl = null;
}
if (innerFrame) {
innerFrame.remove();
// Strip allow-same-origin from the inner frame regardless of what the host requested.
// Full cross-origin isolation requires the sandbox proxy to be served from a different
// origin than the host; stripping allow-same-origin provides partial mitigation when
// the proxy runs same-origin.
const requestedTokens = (params.sandbox || 'allow-scripts allow-forms')
.split(/\s+/)
.filter(Boolean);
innerFrame.sandbox =
requestedTokens.filter((t) => t !== 'allow-same-origin').join(' ') || 'allow-scripts';
const allowParts = [];
if (permissions && typeof permissions === 'object') {
if (permissions.clipboardWrite) allowParts.push('clipboard-write');
if (permissions.camera) allowParts.push('camera');
if (permissions.microphone) allowParts.push('microphone');
if (permissions.geolocation) allowParts.push('geolocation');
}
if (allowParts.length > 0) {
innerFrame.allow = allowParts.join('; ');
}
const cspMeta = buildCspMeta(csp);
const fullHtml = cspMeta ? injectIntoHead(html, cspMeta) : html;
const blob = new Blob([fullHtml], { type: 'text/html' });
innerFrameBlobUrl = URL.createObjectURL(blob);
innerFrame.src = innerFrameBlobUrl;
innerFrame.style.cssText = 'width:100%;height:100%;border:none;display:block';
document.body.appendChild(innerFrame);
}
innerFrame = document.createElement('iframe');
window.addEventListener('beforeunload', () => {
if (innerFrameBlobUrl) {
URL.revokeObjectURL(innerFrameBlobUrl);
}
});
// Strip allow-same-origin from the inner frame regardless of what the host requested.
// Full cross-origin isolation requires the sandbox proxy to be served from a different
// origin than the host; stripping allow-same-origin provides partial mitigation when
// the proxy runs same-origin.
const requestedTokens = (params.sandbox || 'allow-scripts allow-forms').split(/\s+/).filter(Boolean);
innerFrame.sandbox = requestedTokens.filter((t) => t !== 'allow-same-origin').join(' ') || 'allow-scripts';
const allowParts = [];
if (permissions && typeof permissions === 'object') {
if (permissions.clipboardWrite) allowParts.push('clipboard-write');
if (permissions.camera) allowParts.push('camera');
if (permissions.microphone) allowParts.push('microphone');
if (permissions.geolocation) allowParts.push('geolocation');
}
if (allowParts.length > 0) {
innerFrame.allow = allowParts.join('; ');
function injectIntoHead(html, injection) {
if (/<head[^>]*>/i.test(html)) {
return html.replace(/<head([^>]*)>/i, '<head$1>' + injection);
}
if (/<html[^>]*>/i.test(html)) {
return html.replace(/<html([^>]*)>/i, '<html$1><head>' + injection + '</head>');
}
return (
'<!DOCTYPE html><html><head>' + injection + '</head><body>' + html + '</body></html>'
);
}
const cspMeta = buildCspMeta(csp);
const fullHtml = cspMeta ? injectIntoHead(html, cspMeta) : html;
const blob = new Blob([fullHtml], { type: 'text/html' });
innerFrameBlobUrl = URL.createObjectURL(blob);
innerFrame.src = innerFrameBlobUrl;
innerFrame.style.cssText = 'width:100%;height:100%;border:none;display:block';
document.body.appendChild(innerFrame);
}
window.addEventListener('beforeunload', () => {
if (innerFrameBlobUrl) {
URL.revokeObjectURL(innerFrameBlobUrl);
function buildCspMeta(csp) {
// Use an empty object as default so omitted csp still produces a restrictive policy.
const effective = csp && typeof csp === 'object' ? csp : {};
const policy = buildCspPolicy(effective);
if (!policy) return '';
return '<meta http-equiv="Content-Security-Policy" content="' + escapeAttr(policy) + '">';
}
});
function injectIntoHead(html, injection) {
if (/<head[^>]*>/i.test(html)) {
return html.replace(/<head([^>]*)>/i, '<head$1>' + injection);
function buildCspPolicy(csp) {
const resourceDomains = toDomainList(csp.resourceDomains);
const connectDomains = toDomainList(csp.connectDomains) || "'none'";
const frameDomains = toDomainList(csp.frameDomains) || "'none'";
return [
"default-src 'none'",
(
"script-src 'unsafe-inline' 'unsafe-eval' 'wasm-unsafe-eval' blob: data: " +
resourceDomains
).trim(),
("style-src 'unsafe-inline' " + resourceDomains).trim(),
'connect-src ' + connectDomains,
// form-action does not fall back to default-src, so with allow-forms a form could post to
// any origin; bound it to the declared egress allowlist ('none' when none is declared).
'form-action ' + connectDomains,
('img-src data: blob: ' + resourceDomains).trim(),
('media-src ' + (resourceDomains || "'none'")).trim(),
('font-src ' + (resourceDomains || "'none'")).trim(),
'frame-src ' + frameDomains,
"object-src 'none'",
'base-uri ' + (toDomainList(csp.baseUriDomains) || "'self'"),
].join('; ');
}
if (/<html[^>]*>/i.test(html)) {
return html.replace(/<html([^>]*)>/i, '<html$1><head>' + injection + '</head>');
// Only permit host patterns: optional http(s)/ws(s) scheme, optional wildcard subdomain
// prefix, hostname characters, optional port. Rejects CSP keywords and injection attempts.
const SAFE_HOST_RE =
/^(?:(?:https?|wss?):\/\/)?(?:\*\.)?[a-zA-Z0-9][a-zA-Z0-9\-.]*(?::\d{1,5})?$/;
function toDomainList(value) {
if (!Array.isArray(value)) return '';
return value.filter((d) => typeof d === 'string' && SAFE_HOST_RE.test(d.trim())).join(' ');
}
return '<!DOCTYPE html><html><head>' + injection + '</head><body>' + html + '</body></html>';
}
function buildCspMeta(csp) {
// Use an empty object as default so omitted csp still produces a restrictive policy.
const effective = (csp && typeof csp === 'object') ? csp : {};
const policy = buildCspPolicy(effective);
if (!policy) return '';
return '<meta http-equiv="Content-Security-Policy" content="' + escapeAttr(policy) + '">';
}
function escapeAttr(str) {
return str
.replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}
function buildCspPolicy(csp) {
const resourceDomains = toDomainList(csp.resourceDomains);
const connectDomains = toDomainList(csp.connectDomains) || "'none'";
const frameDomains = toDomainList(csp.frameDomains) || "'none'";
return [
"default-src 'none'",
("script-src 'unsafe-inline' " + resourceDomains).trim(),
("style-src 'unsafe-inline' " + resourceDomains).trim(),
"connect-src " + connectDomains,
// form-action does not fall back to default-src, so with allow-forms a form could post to
// any origin; bound it to the declared egress allowlist ('none' when none is declared).
"form-action " + connectDomains,
("img-src data: blob: " + resourceDomains).trim(),
("media-src " + (resourceDomains || "'none'")).trim(),
("font-src " + (resourceDomains || "'none'")).trim(),
"frame-src " + frameDomains,
"object-src 'none'",
"base-uri " + (toDomainList(csp.baseUriDomains) || "'self'")
].join('; ');
}
// Only permit host patterns: optional http(s)/ws(s) scheme, optional wildcard subdomain
// prefix, hostname characters, optional port. Rejects CSP keywords and injection attempts.
const SAFE_HOST_RE = /^(?:(?:https?|wss?):\/\/)?(?:\*\.)?[a-zA-Z0-9][a-zA-Z0-9\-.]*(?::\d{1,5})?$/;
function toDomainList(value) {
if (!Array.isArray(value)) return '';
return value
.filter(d => typeof d === 'string' && SAFE_HOST_RE.test(d.trim()))
.join(' ');
}
function escapeAttr(str) {
return str.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
notifyReady();
</script>
</body>
notifyReady();
</script>
</body>
</html>