Article

Content-Security-Policy (CSP): A Practical Guide

A Content-Security-Policy is an allow-list that tells the browser where scripts, styles and other resources may load from — the single strongest defence against cross-site scripting. Start from default-src 'self', deploy in report-only mode, then tighten and enforce.

By Paul Rudenko, Security ResearcherUpdated Jun 26, 20268 min read

A Content-Security-Policy is an allow-list that tells the browser where scripts, styles and other resources may load from — the single strongest defence against cross-site scripting. Start from default-src 'self', deploy in report-only mode, then tighten and enforce.

For the site owner (plain English)

If you are not a developer, here is what matters. A Content-Security-Policy is a safety instruction your site sends to visitors' browsers that blocks unauthorised scripts — the main way attackers steal data or hijack a logged-in session. You do not write code to get one; it is a response header your web host, CDN (such as Cloudflare), or platform can add. The catch is that a policy which is too strict can break your own site's scripts, so it should be switched on in "report-only" mode first and tightened gradually — not enabled all at once. If you use a managed platform or a security plugin, check whether it can set CSP for you, and lean on your host or developer to roll it out in stages. To see whether your site sends one today, run the security headers checker.

What does a CSP do?

Content-Security-Policy (CSP) is an HTTP response header that instructs the browser to treat the page's content as governed by an explicit allow-list. Instead of letting a page load scripts, styles, images, fonts and frames from anywhere, the browser only permits the origins you name. Anything outside the list is blocked before it ever runs.

The reason this matters is cross-site scripting (XSS). If an attacker manages to inject a<script> tag or an inline event handler into your page — through an unescaped comment field, a query parameter reflected into HTML, or a compromised third-party widget — the browser would normally execute it with the full privileges of your origin. A well-written CSP changes the default answer from "run it" to "block it." The injected script does not match any allowed source, so it never executes, the data exfiltration never happens, and the incident is reduced to a logged report instead of a breach.

CSP does not fix the underlying injection bug — you still want to escape output and validate input — but it is a powerful second layer. Even a site with a latent XSS flaw can be protected in practice if its CSP refuses to run unauthorised code.

Which directives matter most?

A policy is a list of directives, each naming a resource type and the sources allowed for it. Directives are separated by semicolons. The ones you will reach for most often:

  • default-src — the fallback for any resource type you don't name explicitly. Set it to 'self' and treat everything else as a deliberate exception.
  • script-src — where JavaScript may load from. This is the most security-critical directive; keep it tight and avoid 'unsafe-inline'.
  • style-src — where CSS may load from, including inline <style>blocks and style attributes.
  • img-src — allowed image origins. Often broadened to include a CDN ordata: URIs.
  • connect-src — endpoints reachable via fetch, XHR, WebSocket andEventSource. Important for locking down where data can be sent.
  • frame-ancestors — who may embed your page in a frame. The modern replacement forX-Frame-Options, and your defence against clickjacking.
  • base-uri — restricts the <base> element so an attacker cannot rewrite relative URLs. Set it to 'self' or 'none'.
  • object-src 'none' — blocks <object>,<embed> and legacy plugin content. There is almost never a reason to allow it, so default to 'none'.

The full directive reference is maintained by MDN Web Docs, which is the authoritative place to check exact semantics and browser support.

How do nonces and hashes work?

The hard part of any real CSP is inline script and style. Many sites carry inline<script> blocks — analytics snippets, framework bootstrap code, JSON state — and the lazy fix is to add 'unsafe-inline' to script-src. That re-opens the exact door CSP was meant to close, because injected inline script is also "inline" and would then be allowed.

The correct tools are nonces and hashes. A nonce is a random, unguessable token your server generates fresh for every response. You put it in the header and on each trusted inline tag; the browser runs only the inline scripts whosenonce attribute matches.

<!-- Server generates a new random value per request -->
Content-Security-Policy: script-src 'nonce-r4nd0m2026';

<script nonce="r4nd0m2026">
  // trusted bootstrap code runs
</script>

<!-- Injected by an attacker — no matching nonce, so it is blocked -->
<script>stealCookies()</script>

A hash is the alternative when the inline content is static. You compute a SHA-256 digest of the exact script body and list it as'sha256-…' in script-src. The browser hashes each inline block and runs only those that match. Hashes need no per-request server work, but they break the moment the script content changes by even one character — so nonces are usually the better fit for dynamic pages, hashes for fixed snippets.

A policy with script-src 'unsafe-inline' technically "has a CSP," and a scanner will report the header as present — but it provides almost no XSS protection, because injected inline script is allowed right alongside yours. Just as bad: reusing a single nonce across requests. A nonce is only safe if an attacker cannot predict it, and a static value baked into your template is fully predictable. Generate a fresh nonce per response, or use hashes for static content.

How do I roll out CSP without breaking my site?

The fear with CSP is breaking your own site — a misjudged directive can silently block a legitimate script and leave a feature dead. The answer is to roll out in monitoring mode first. The Content-Security-Policy-Report-Only header applies your policy without enforcing it: nothing is blocked, but every violation is reported.

Pair it with a reporting endpoint so you can see what the policy would have blocked. The modern mechanism is report-to (configured via the Reporting-Endpointsheader); the older, still widely supported one is report-uri. Sending both maximises coverage across browsers.

Reporting-Endpoints: csp-endpoint="https://example.com/csp-reports"
Content-Security-Policy-Report-Only: default-src 'self'; report-to csp-endpoint; report-uri /csp-reports

Leave report-only running for days or weeks under real traffic, read the reports, and adjust: whitelist the legitimate origins your reports reveal, attach nonces to your own inline scripts, and trim anything you don't recognise. Only when the violation stream is quiet — meaning no legitimate resource is being blocked — do you switch the header name to the enforcingContent-Security-Policy. Reading reports before enforcing is what turns CSP from a risky big-bang into a controlled, observable migration.

What does a good starter policy look like?

There is no universal policy, but this is a reasonable, restrictive baseline for a site that serves its own scripts and styles and uses per-request nonces. Adjust the source lists to match the CDNs and APIs you genuinely depend on.

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{{REQUEST_NONCE}}';
  style-src 'self' 'nonce-{{REQUEST_NONCE}}';
  img-src 'self' data:;
  connect-src 'self';
  frame-ancestors 'none';
  base-uri 'self';
  object-src 'none';
  upgrade-insecure-requests

This blocks plugin content, forbids framing, pins the base URL, upgrades stray HTTP subresources to HTTPS, and allows scripts and styles only from your own origin plus the per-request nonce. It is a starting point to deploy in report-only mode — not a final answer — but it is far stronger than the 'unsafe-inline' policies that remain common in the wild.

Verify your policy

After deploying, confirm the header is actually being sent and that it is doing what you intend. Check three things: the header is present on real responses (not just the HTML document but the responses you care about), it does not contain 'unsafe-inline' inscript-src, and your report endpoint is receiving data. Browser developer tools log every CSP violation to the console, which is the fastest way to catch a directive that is too tight during testing.

See whether your CSP and other security headers are present and well-formed.

Check your security headers

CSP is one header in a larger set. For the full picture, read the pillar guide on security headers, and pair CSP with strict transport security — see the HSTS and preload guide.

Frequently asked questions

What does Content-Security-Policy do?

Content-Security-Policy is an HTTP response header that gives the browser an allow-list of where each kind of resource may load from — scripts, styles, images, fonts, frames and network connections. Anything not on the list is blocked before it runs. Its primary purpose is to defend against cross-site scripting: if an attacker injects a script into your page, the browser refuses to execute it because it does not match any allowed source. CSP does not replace escaping output or validating input — the injection bug still exists — but it is a strong second layer that can stop an injected payload from doing damage. A good policy starts from default-src 'self', names only the origins you genuinely depend on, and avoids 'unsafe-inline'.

How do I add a CSP without breaking my site?

Deploy it in report-only mode first. Send the policy with the Content-Security-Policy-Report-Only header instead of the enforcing one: the browser evaluates the policy and reports every violation, but blocks nothing, so your site keeps working while you observe. Pair it with a reporting endpoint using report-to (and report-uri for older browsers) so you can collect the violations. Run it under real traffic for days or weeks, read the reports, and adjust — whitelist legitimate origins, attach nonces to your own inline scripts, and remove anything you don't recognise. Once the violation stream is quiet, meaning no legitimate resource is being blocked, switch the header name to Content-Security-Policy to enforce it. This turns a risky change into a controlled, observable migration.

What is a CSP nonce?

A nonce is a random, unguessable token your server generates fresh for every single response. You include it in the CSP header as 'nonce-…' and add a matching nonce attribute to each inline script or style tag you trust. The browser runs only the inline blocks whose nonce matches the one in the header, and blocks the rest. This lets you keep necessary inline scripts without resorting to 'unsafe-inline', which would allow any injected inline script too. The critical rule is freshness: a nonce only works if an attacker cannot predict it, so you must generate a new value per response. Reusing one static nonce across requests — or baking it into a template — makes it guessable and defeats the entire purpose.

Why is 'unsafe-inline' bad?

'unsafe-inline' tells the browser to allow any inline script or style on the page, regardless of where it came from. The problem is that injected code is also inline. If an attacker manages to insert a script tag or an inline event handler into your HTML, 'unsafe-inline' permits it to run right alongside your own code — which is exactly the cross-site scripting attack CSP exists to stop. A policy with script-src 'unsafe-inline' technically has a CSP, and a scanner will report the header as present, but it provides almost no real XSS protection. The fix is to remove 'unsafe-inline' and authorise your trusted inline code with per-request nonces or content hashes instead, so only the specific blocks you intended can execute.

Related guides

See your whole external attack surface

One page is a start. The full external scan covers TLS, headers, DNS, exposed files, open services and known-exploited CVEs across your whole domain.

See the full scan →