CSP meta tag vs HTTP header
CentralCSP Team ·
Last update:
You can deliver a Content Security Policy (CSP) two ways: as an HTTP response
header, or as a <meta> tag in your HTML. They look interchangeable, and for a
simple default-src 'self' they mostly are. But the header is the real one. It
applies earlier and supports every part of the policy, while a meta tag starts
late and silently drops several directives. This post shows both, explains the
gaps, and tells you when a meta tag is an acceptable fallback.
The two ways to deliver a policy
As a header, sent with the response before any HTML is parsed:
Content-Security-Policy: default-src 'self'; script-src 'self'As a meta tag, which must sit inside <head>:
<head>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self'">
</head>Both are standard and widely supported. The difference is not whether they work, it is what each one can do and when it kicks in.
| HTTP header | <meta> tag | |
|---|---|---|
| Applies from | Before the first byte of HTML is parsed | Only to content after the meta element |
| Covers preloaded resources | ✅ Yes | ❌ No |
frame-ancestors | ✅ Enforced | ❌ Ignored |
report-uri | ✅ Enforced | ❌ Ignored |
sandbox | ✅ Enforced | ❌ Ignored |
| Report-Only mode | ✅ Content-Security-Policy-Report-Only | ❌ No equivalent |
| Violation reporting | ✅ Via Reporting-Endpoints | ❌ Cannot set a response header |
| Changeable after parsing | ❌ No (nor is it meant to be) | ❌ No, edits to content are ignored |
| Needs server or CDN config | Yes | No, it is just markup |
Everything below explains the rows.
Why the header is the default
A meta policy starts late
A header is delivered before the browser parses a single byte of your HTML, so it governs the whole document, including resources the browser preloads. A meta tag is just markup: the policy only applies to content the parser reaches after the meta element. Anything before it is not covered.
That is a real security gap, not a technicality. If a script tag sits above your
meta tag, or an attacker injects markup earlier in the document, it runs before the
policy exists. CSP Level 3 §3.3 is explicit
that policies delivered in meta elements are not applied to content that precedes them,
which is why the meta tag has to go as early in <head> as possible. You also cannot change a meta policy after the fact: editing
the content attribute with JavaScript after parsing is ignored.
Some directives only work in a header
Three directives are defined to be ignored when the policy comes from a meta tag:
frame-ancestors, which controls who can embed your page (your clickjacking defense).report-uri, the legacy reporting directive.sandbox, which applies sandbox restrictions to the document.
Put any of those in a meta tag and the browser drops them while keeping the rest of the policy. So a meta-delivered policy cannot protect against framing at all.
No report-only, no reporting
You also cannot run a meta policy in test mode. The
Content-Security-Policy-Report-Only
header has no meta-tag equivalent, so the safe way to roll out a policy, watch
violations without blocking anything, is header-only.
Reporting does not really work from a meta tag either. The modern
report-to
directive names an endpoint that you define with the
Reporting-Endpoints
response header (see Report-To vs Reporting-Endpoints for how the endpoint headers differ),
and a meta tag cannot set a response header, so there is nowhere
for reports to go. If you want violation reports, you want headers. See
get started with CSP reporting for the full
setup.
When a meta tag is acceptable
Sometimes you genuinely cannot set response headers: a static host or CDN that does not let you configure them, a CMS where you only control the page template, a quick prototype. In those cases a meta tag is better than no policy at all. Keep two things in mind:
- Put the meta tag first in
<head>, before any other tag that loads a resource, so the gap before it is as small as possible. - Accept that you lose
frame-ancestors, report-only testing, and reporting. For clickjacking protection when you cannot set a CSP header, the olderX-Frame-Optionsheader is sometimes available even when the host limits which headers you can set. It is one of several legacy security headers worth retiring once a CSP covers the same ground.
Using both: they stack, they do not merge
If you send a header policy and a meta policy at the same time, the browser does not combine them into one. It enforces both, independently, and a resource has to satisfy every policy to load. The practical result is the intersection: the most restrictive rule wins, and a second policy can only add restrictions, never loosen them.
Content-Security-Policy: default-src 'self'<meta http-equiv="Content-Security-Policy" content="default-src 'self' https://cdn.example">Here the meta tag allows https://cdn.example, but the header does not, so the
resource is still blocked. The meta policy cannot widen what the header allows. Two
enforced policies always combine this way, by intersection.
The recommendation
Use a header. Deliver your Content Security Policy with the
Content-Security-Policy response header, start in report-only to shake out false
positives, and add reporting so you can see what the policy blocks. Reach for a meta
tag only when you cannot set headers, and know what you are giving up. CentralCSP's free
CSP scanner checks whatever you ship, header or meta, and tells you
which directives a meta-delivered policy is silently dropping; once you are on headers,
CentralCSP collects the violation reports the meta tag could never have sent.
Next steps
- New to policies? Start with get started with Content Security Policy.
- Ready for a full policy? See how to build a strong CSP.
- Want violation reports? Get started with CSP reporting, then create a free account.