Simon Willison recently published a test exploring whether JavaScript can escape a CSP meta tag by creating an iframe. The short answer is yes, in most browsers, it can. The longer answer requires understanding why the CSP spec treats meta-tag policies differently from header-delivered ones, and what that distinction costs you in practice.
Two Ways to Set a CSP, Two Different Security Guarantees
Content Security Policy can be delivered in two ways: as an HTTP response header (Content-Security-Policy: script-src 'none') or as an HTML meta tag (<meta http-equiv="Content-Security-Policy" content="script-src 'none'">). These look equivalent. They are not.
The HTTP header arrives before the browser parses a single byte of HTML. The policy is established before the parser runs, before any inline scripts have a chance to execute, before any subresources are requested. The meta tag, by contrast, is parsed inline as the HTML parser walks the document. Any script or resource that appears in the markup before the meta tag fires before the policy is active. This is why the CSP Level 3 spec explicitly forbids several directives from being expressed in meta tags at all: frame-ancestors, report-uri, and sandbox cannot appear in a meta-tag policy. The spec authors recognized that some directives require server-level delivery to be meaningful.
But there is a subtler issue that the spec handles less cleanly: what happens when the page creates an iframe?
How CSP Propagates Into Iframes
For iframes loaded from a URL, the rules are straightforward. The child document is governed by its own response headers, not its parent’s. The parent can impose additional restrictions using the sandbox attribute on the <iframe> element, but the parent’s CSP does not automatically flow into the child.
For srcdoc iframes, the situation is different. A srcdoc iframe has no URL. Its content is specified directly as an attribute:
<iframe srcdoc="<p>Hello</p>"></iframe>
Because there is no URL, there are no response headers, and the iframe inherits the parent’s origin. The HTML spec says a srcdoc iframe’s origin is determined by its parent. The CSP spec says a srcdoc document’s policy should be inherited from the parent’s policy. That inheritance works correctly when the parent’s CSP was delivered via an HTTP header. When the CSP came from a meta tag, browsers have historically not propagated it to srcdoc frames.
The result is a gap: create a srcdoc iframe in JavaScript, and its content may execute without any of the restrictions the parent page’s meta-tag CSP was meant to enforce.
The Escape in Practice
Consider a page served from a static host where custom HTTP headers are not configurable. The developer adds a meta tag to restrict script execution:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Security-Policy" content="script-src 'none'">
</head>
<body>
<!-- user-supplied HTML is rendered here -->
</body>
</html>
This seems like a reasonable precaution. But if an attacker can inject HTML into this page, they can do this:
<iframe srcdoc="<script>alert(document.cookie)</script>"></iframe>
The browser creates the srcdoc iframe. That iframe does not inherit the parent’s meta-tag CSP. The script runs. The CSP did not protect anything.
You can also construct this escape from JavaScript running in the parent page, which is relevant in a different threat model: suppose the page itself has a script that builds a sandboxed preview environment using srcdoc:
const iframe = document.createElement('iframe');
iframe.srcdoc = userSuppliedHTML;
document.body.appendChild(iframe);
If the developer believes the meta-tag CSP on the parent will constrain what runs inside that iframe, they are wrong in most browser environments. The iframe needs its own explicit sandboxing.
What the Spec Actually Says
The CSP Level 3 spec defines how a document’s policy is determined in Section 3.4, “Obtain the policy for a document”. For a srcdoc document, the spec says the policy should be the navigation response’s policy, falling back to the parent’s policy if there is no navigation response. Since srcdoc has no navigation response, the parent’s policy should apply.
The spec says this should work. Browsers have not always implemented it correctly for meta-tag-sourced policies, and the discrepancy between spec and implementation is where the escape lives.
Filing a bug against a browser for not propagating meta-tag CSP into srcdoc iframes is technically valid. The fix would be for browsers to treat meta-tag policies as equivalent to header policies for the purpose of srcdoc inheritance. But several years of bugs, patches, and regressions across Chrome, Firefox, and Safari have produced inconsistent behavior. As of mid-2025, you should not rely on cross-browser consistency here.
The Directives You Cannot Use in a Meta Tag
Beyond the srcdoc inheritance issue, the directives that are explicitly excluded from meta-tag CSP matter a great deal in practice.
frame-ancestors is probably the most important. This directive controls which origins can embed your page in an iframe, providing clickjacking protection. It does not work in a meta tag. If you need clickjacking protection and you are on a static host, you are out of luck unless your host gives you access to response headers. GitHub Pages, for example, does not allow arbitrary header customization. Netlify and Vercel do, through their respective configuration files.
report-uri and its successor report-to are similarly restricted. They allow the browser to send CSP violation reports to an endpoint. In a meta tag, these directives are silently ignored by most browsers, meaning you also lose observability for your policy.
The Sandbox Attribute Is a Separate Mechanism
The sandbox attribute on <iframe> elements is often confused with CSP, but it operates independently. Where CSP restricts what resources can be loaded and executed based on their origin, sandbox restricts what the framed document can do, regardless of where its resources come from.
A sandboxed iframe with no allow-* tokens disables JavaScript entirely, blocks form submission, prevents popups, and removes same-origin access. Adding allow-scripts re-enables JavaScript. Adding both allow-scripts and allow-same-origin effectively removes sandboxing, because a same-origin script can remove the sandbox by navigating its frame.
The correct defensive pattern for a srcdoc iframe that renders user-supplied HTML is:
const iframe = document.createElement('iframe');
iframe.sandbox = 'allow-scripts'; // without allow-same-origin
iframe.srcdoc = userSuppliedHTML;
document.body.appendChild(iframe);
With allow-scripts but without allow-same-origin, the iframe’s JavaScript cannot access the parent’s DOM, cannot read cookies, and cannot navigate the top frame. This is independent of whether any CSP is present, and it works whether you set your CSP via header or meta tag.
The lesson here is that sandbox on the iframe element is more reliable than hoping CSP propagates correctly into srcdoc frames. Use both where possible, but if you can only use one, sandbox is the one that actually works consistently across browsers.
Where This Matters in Real Deployments
The environments most affected by this gap are the ones that developers might expect to be least vulnerable.
Static documentation sites and developer tools often render code examples and user-supplied content. Many of these sites live on CDNs or static hosts where HTTP headers cannot be customized. The developers who built them likely added a meta-tag CSP as a security precaution, not realizing it provides weaker guarantees than they expected.
Jupyter-style notebook environments and REPL tools frequently use srcdoc iframes to isolate cell output. If the parent frame’s security posture depends on a meta-tag CSP rather than a header-delivered one, that isolation is not what the developer thinks it is.
Rich text editors and comment rendering systems that allow HTML input and display it in the same origin as the application are a higher-risk case. If the sanitization pipeline relies on CSP as a last line of defense, and that CSP is meta-tag-delivered, an attacker who bypasses sanitization can create a srcdoc iframe and execute arbitrary script.
The csp Attribute on Iframes
The CSP Embedded Enforcement spec defines a csp attribute on <iframe> elements that lets a parent require a minimum CSP in the child document:
<iframe src="child.html" csp="script-src 'none'"></iframe>
This does not apply CSP to the child. It tells the browser to refuse to load the child’s document unless that document itself has a CSP at least as restrictive as what the attribute specifies. It is a mechanism for a parent to enforce that its embedded documents have their own security policies.
As of mid-2025, this attribute is supported only in Chromium-based browsers. Firefox and Safari have not implemented it. This makes it unsuitable as a primary defense in cross-browser applications, though it is worth adding as defense-in-depth in Chromium-only tooling.
What to Do
If you control your server or hosting configuration, set CSP via HTTP headers, not meta tags. This is unambiguously the right call. Netlify, Vercel, Cloudflare Pages, and most modern static hosts provide ways to configure custom response headers.
If you are stuck with a static host that offers no header customization, meta-tag CSP is still worth having for the protections it does provide, but understand that it does not protect srcdoc iframe content and that frame-ancestors and reporting will not work. Compensate by adding explicit sandbox attributes to every iframe you create, and do not rely on CSP as a complete sandbox boundary.
If your application renders user-supplied HTML anywhere, treat CSP as one layer in a defense-in-depth stack, not a complete solution. Sanitization at the input layer, sandbox on iframe elements, and CSP via HTTP headers together produce a substantially stronger posture than any single mechanism.
Willison’s test is a good reminder that the web’s security mechanisms have implementation gaps that diverge from what the spec mandates. The gap between “the spec says this should be inherited” and “browsers implement inheritance consistently” is exactly where security assumptions break down.