What Is a CSP Header? Content Security Policy Explained
What a Content-Security-Policy header actually does, how directives work, and how to read one without guessing what each source list means.
Try it now: HTTP Header Analyzer — Paste response headers from curl or DevTools and get them explained, with missing security headers, CSP weaknesses, cookie flags and cache contradictions flagged.
What a CSP Header Actually Does
A Content-Security-Policyheader is an allowlist the server sends and the browser enforces. It names, directive by directive, which sources are permitted to supply which kind of content — scripts from this origin, styles from that one, images from anywhere, network requests only back to your own API — and the browser refuses to load or execute anything that falls outside the list. Nothing about content security policy is advisory: a script tag pointing at a source not on the allowlist doesn't get a warning in the console and run anyway, it simply doesn't execute.
That enforcement point is what makes CSP worth the setup cost. Most security headers describe a preference — do this over HTTP, don't frame this page. CSP is closer to a firewall rule for a single page's content sources, applied by every browser that loads it, with no server-side trust required after the header is sent.
Why It Exists: Defense-in-Depth Against XSS
CSP's reason for existing is cross-site scripting, and it's worth being precise about what it does and doesn't do there. It does not prevent an attacker from injecting a <script> tag into your page through a stored or reflected XSS bug — an unescaped comment field, a template that echoes a query parameter back into HTML, any of the usual ways user input ends up rendered as markup. Fixing that injection point is still the actual fix, and CSP is not a substitute for input sanitization or output encoding.
What a strict CSP does is make the injected script harmless on arrival. If script-src doesn't allow inline scripts and doesn't allow the attacker's source, the injected tag sits in the DOM and never runs. The browser parses it, sees it doesn't satisfy the policy, and drops it. This is the defense-in-depth argument for CSP in one sentence: it doesn't stop the injection, it stops the injected code from executing — a second, independent line of defense that holds even when the first one (clean input handling) has already failed somewhere in the codebase.
Reading a Real CSP Header, Directive by Directive
Here's a realistic policy for a page that serves its own scripts and styles, loads images from a CDN, and calls its own API:
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0mBase64'; style-src 'self' 'unsafe-inline'; img-src 'self' https://cdn.example.com data:; connect-src 'self' https://api.example.com
default-src 'self'— the fallback for any directive type not listed explicitly. Fonts, media, workers and anything else without its own rule fall back to this one, so it's the baseline the rest of the policy narrows or widens from.script-src 'self' 'nonce-r4nd0mBase64'— the most security-critical directive, since it's the one that decides whether an injected<script>tag can run at all. This policy allows scripts served from the same origin plus one specific inline script tagged with a matchingnonceattribute — nothing else.style-src 'self' 'unsafe-inline'— same-origin stylesheets, plus inlinestyleattributes and<style>blocks. Inline styles are a much smaller attack surface than inline scripts, which is why'unsafe-inline'shows up here far more often than onscript-src.img-src 'self' https://cdn.example.com data:— images from the same origin, a named CDN, anddata:URIs (inline base64 images), a common allowance since images can't execute code the way scripts can.connect-src 'self' https://api.example.com— the directive that restrictsfetch,XMLHttpRequest, WebSocket and EventSource targets. This is the one that catches exfiltration: even a successfully injected script that runs under a permissivescript-srcstill can't send stolen data anywhereconnect-srcdoesn't allow.
The keyword values are worth knowing on sight. 'self' means same origin only — same scheme, host and port as the page. 'unsafe-inline' allows inline scripts or styles with no further check, which is exactly as broad as it sounds; a lot of real sites still ship it on script-src because migrating away from inline event handlers and inline <script> blocks is real refactoring work, not a header change. 'nonce-...' and 'sha256-...' are the safer alternative: a nonce is a random value generated fresh per response and matched against a nonceattribute on the script tag, while a hash allowlists a specific script's exact content. Both let a specific inline script run without opening the door to every inline script an attacker might inject.
Report-Only Mode: Finding Out What a Policy Would Break
Shipping a strict CSP directly to production is how a page ends up with broken analytics, a dead payment widget, or fonts that silently stop rendering — because some third-party script the team forgot about needed a directive that wasn't on the list. The header Content-Security-Policy-Report-Only exists specifically to avoid that: it reports every violation a strict policy would produce, via the report-uri or report-to directive, without blocking anything. Nothing the page actually does changes; you get a log of everything the enforced version would have stopped.
The practical rollout most teams follow is to run report-only against production traffic for a few days, read the violation reports, add the legitimate sources that show up, and only then flip the header to the enforcing Content-Security-Policy. Skipping that step is the single most common reason a CSP rollout gets reverted a week after launch.
Checking a Policy Without Deploying It First
Before it goes anywhere near report-uri tracking, a policy is worth reading the way this article just walked through one: directive by directive, checking that script-srcdoesn't quietly include 'unsafe-inline' next to a nonce (which defeats the nonce, since browsers that support nonces ignore 'unsafe-inline' only when a nonce or hash is present — older browsers honor both), that default-src isn't missing entirely, and that a wildcard like https: hasn't been used as a shortcut that allows scripts from any HTTPS host. GenKitLab's HTTP Header Analyzer does this reading for you: paste response headers copied from curl or DevTools and it explains each one, flags missing security headers, calls out specific CSP weaknesses like an open script-src or a nonce sitting next to 'unsafe-inline', and checks cookie flags and cache header contradictions in the same pass — a broader security headers review than a CSP checker alone. It runs entirely client-side; the headers you paste are never uploaded anywhere. If the policy in question is protecting a page that also issues auth tokens, the JWT Decoder is the companion check — CSP stops a stolen token from being read by injected script in the first place, but decoding the token itself is a separate question worth its own tool.
Frequently asked questions
›What is a CSP header, in one sentence?
It's a Content-Security-Policy response header that tells the browser which sources are allowed to supply scripts, styles, images and other content for a page, and the browser blocks anything not on that allowlist.
›Does CSP stop XSS attacks?
Not the injection itself — that's still a job for input sanitization and output encoding. What CSP stops is the injected code from executing: if script-src doesn't allow inline scripts or the attacker's source, a successfully injected <script> tag simply doesn't run. It's a defense-in-depth layer, not a substitute for fixing the injection point.
›What's the difference between 'unsafe-inline' and a nonce?
'unsafe-inline' allows every inline script or style on the page with no further check — broad and easy to add, but it defeats CSP's protection against injected scripts, since an attacker's inline script is inline too. A nonce is a random value generated fresh per response and matched against a specific script tag's nonce attribute, so only that one inline script runs and an injected one — which won't have the correct nonce — is still blocked.
›What does Content-Security-Policy-Report-Only do?
It reports every violation a strict policy would trigger, without blocking anything. Teams use it to find out what a policy would break — a forgotten analytics script, a third-party widget — before switching to the enforcing Content-Security-Policy header.
›Which CSP directive matters most for security?
script-src, since it decides whether an injected script can execute at all. connect-src matters almost as much in practice, because it restricts where fetch, XHR and WebSocket calls can go — the directive that limits what a script can exfiltrate even if it does manage to run.
›Is there a tool to check a CSP header for weaknesses?
GenKitLab's HTTP Header Analyzer parses pasted response headers and flags CSP-specific issues — an overly broad script-src, a nonce undermined by a co-present 'unsafe-inline', a missing default-src — alongside other missing security headers, cookie flag problems and cache contradictions. It runs entirely in the browser, so nothing pasted is uploaded.
Last updated