Web Security

What Is a Content Security Policy (CSP) and How to Set One

A practical guide to Content Security Policy: the header that limits which sources can load on your site, mitigates XSS, and how to roll one out safely.

StackOptic Research Team28 Apr 20268 min read
What a Content Security Policy is and how to set one

A Content Security Policy is the single most effective defence a website can deploy against cross-site scripting — and one of the most commonly skipped, because it has a reputation for being fiddly. In plain terms, a CSP is an HTTP header that tells the browser exactly which sources are allowed to load scripts, styles, images and other content. Anything from an origin you have not approved simply will not load or run. This guide explains what a CSP is, the directives that matter, how nonces and hashes work, and — most importantly — how to roll one out without breaking your site.

It is the deep dive on one header from the broader HTTP security headers explained guide, and a core part of how to protect your website from common attacks.

The problem CSP solves

To understand CSP, you have to understand cross-site scripting (XSS), which sits near the top of the OWASP Top 10 for good reason. XSS happens when an attacker manages to get malicious JavaScript to run in your users' browsers — through an unsanitised input, a compromised third-party tag, a malicious advert, or a vulnerable dependency. Once that script runs, it has the same access as your own code: it can read cookies and tokens, harvest what users type into forms, alter the page, or redirect victims elsewhere.

The traditional defence is to sanitise every input and encode every output so injected script never gets in. That is essential, but it is also fragile — a single missed spot, anywhere in your stack or your third parties, reopens the door. CSP adds a second wall: even if a malicious script does get injected, the browser refuses to execute it because its source is not on your allow-list. This is defence in depth — the assumption that a layer will eventually fail, so you put another behind it.

How a CSP works

A CSP is delivered as the Content-Security-Policy response header (or, less commonly, a <meta> tag). Its value is a series of directives, each naming a resource type and the sources allowed for it. The browser reads the policy and enforces it on every resource the page tries to load. If the page attempts to load a script from evil.example and your script-src does not list that origin, the browser blocks it and (optionally) reports the violation.

Sources can be specific origins (https://cdn.example.com), keywords like 'self' (the page's own origin) or 'none' (nothing allowed), or — for inline code — a nonce or hash (covered below). The art of writing a CSP is enumerating every legitimate source your site genuinely uses, and nothing more.

The directives that matter

You do not need every directive, but a strong policy uses these:

  • default-src — the fallback for any resource type you do not specify explicitly. Setting default-src 'self' is a sensible, restrictive baseline.
  • script-src — where scripts may load from. This is the most important directive for XSS defence; keep it tight.
  • style-src — where stylesheets may load from.
  • img-src — where images may load from.
  • connect-src — which origins the page may make network requests to (fetch, XHR, WebSocket). Limits where injected code could exfiltrate data.
  • font-src — where web fonts may load from.
  • frame-ancestors — who may embed your page in a frame. Setting frame-ancestors 'none' (or specific origins) is the modern way to prevent clickjacking, superseding X-Frame-Options.
  • base-uri — restricts the <base> element, blocking an attack where injected markup rewrites relative URLs. base-uri 'self' is a good default.
  • object-src 'none' — disables legacy plugin content (<object>, <embed>); almost no modern site needs it, and disabling it removes a class of risk.
  • form-action — restricts where forms may submit, limiting data theft via injected forms.
  • upgrade-insecure-requests — instructs the browser to upgrade http:// resource requests to HTTPS, helping with mixed content.
DirectiveControlsSensible starting value
default-srcFallback for unset types'self'
script-srcJavaScript sources'self' + nonce/hash
style-srcStylesheet sources'self' (+ nonce/hash if needed)
connect-srcfetch/XHR/WebSocket targets'self' + your APIs
img-srcImage sources'self' data: + CDNs
frame-ancestorsWho can frame you'none' or trusted origins
base-uri<base> element'self'
object-srcPlugins/embeds'none'

Inline scripts: nonce, hash, and 'strict-dynamic'

The hardest part of CSP is inline scripts and styles. By default, a strict script-src blocks all inline script — including your own legitimate inline code. The lazy fix is to add 'unsafe-inline', but doing so allows any inline script, including whatever an attacker injects, which largely defeats the purpose of the policy. Avoid it.

The secure ways to allow specific inline scripts are:

  • Nonce. Generate a random value on every page load, put it in the CSP header (script-src 'nonce-RANDOM') and on each trusted <script nonce="RANDOM">. The browser runs only scripts whose nonce matches. Because the value changes each load and an attacker cannot guess it, injected scripts have no valid nonce. This suits dynamic, server-rendered pages.

  • Hash. Compute a cryptographic hash (e.g. SHA-256) of an inline script's exact contents and list it in the policy (script-src 'sha256-...'). The browser runs only inline scripts whose contents match that hash. This suits static scripts that never change.

  • 'strict-dynamic'. This keyword says: trust scripts loaded by an already-trusted script (one with a valid nonce or hash), and ignore host-based allow-lists for scripts. It makes complex sites with script loaders manageable under a strict policy, and is the recommended modern approach for script-src on demanding sites.

The Mozilla Developer Network (MDN) documents every directive and source expression in detail, and is the authoritative reference when you are building a policy.

Roll it out safely: Report-Only first

Here is the rule that prevents disasters: never enforce a brand-new CSP on a live site blind. A strict policy enforced before you know what your site actually loads will block legitimate scripts, styles and third-party tools, and break things in ways your users notice before you do.

Instead, use Content-Security-Policy-Report-Only. In this mode the browser does not block anything — it only reports what would have been blocked, sending each violation to an endpoint you specify (via the report-to / report-uri mechanism). The rollout looks like this:

  1. Draft a candidate policy that reflects what you believe your site loads.
  2. Deploy it in Report-Only mode and set up a reporting endpoint (or use a reporting service).
  3. Watch the reports for a representative period — every page, every flow, every third party. Real violations reveal sources you forgot.
  4. Refine the policy: add the legitimate sources you missed, and scrutinise anything suspicious.
  5. Switch to enforcement (Content-Security-Policy) only once reports show no legitimate functionality would break.

This staged approach turns CSP from a risky big-bang change into a controlled, observable rollout. It takes patience, but it is the difference between a policy that protects you and a policy that takes your checkout offline.

Checking and verifying your CSP

You can read any site's CSP the same way you read other headers. In the browser, open DevTools → Network, click the main document request, and look at the response headers for Content-Security-Policy. From a terminal, curl -I https://example.com shows the headers, and CSP violations also appear in the DevTools Console as the browser blocks (or, in report-only mode, would block) resources — which is the fastest way to debug a policy interactively. Online graders such as security-header checkers and Mozilla Observatory will assess whether your CSP exists and flag obvious weaknesses like 'unsafe-inline'. Broader audits — StackOptic included — report whether a meaningful CSP is present as part of a header grade, alongside performance and SEO. For the wider header context, see how to read a website's HTTP headers.

Common pitfalls

  • Allowing 'unsafe-inline' in script-src — it looks like a CSP but provides little XSS protection. Use nonces or hashes instead.
  • Allowing 'unsafe-eval' unless you genuinely need it — it permits string-to-code execution, widening the attack surface.
  • A wildcard or over-broad policy (script-src * or huge allow-lists) — a permissive policy passes a grader but barely protects anyone.
  • Enforcing before report-only testing — the classic way to break a site and roll the whole thing back, souring the team on CSP.
  • Forgetting third parties — analytics, tag managers, chat widgets, fonts and ad code all need their origins listed; missing one breaks that feature.
  • Setting it once and forgetting — adding a new third-party tool later will be silently blocked by an old policy, so update the CSP whenever your dependencies change.
  • Omitting frame-ancestors, base-uri and object-src — these close real, cheap gaps and are easy to add.

A pragmatic starting policy

For many sites, a reasonable report-only starting point is something like default-src 'self'; script-src 'self' 'strict-dynamic' 'nonce-...'; style-src 'self'; img-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; object-src 'none', then widen connect-src, img-src and friends based on what the reports show your site genuinely uses. Begin restrictive and loosen only where the evidence requires it — that is far safer than starting permissive and trying to tighten later, because a permissive policy gives you no signal about what you could safely remove. The goal is the tightest policy your site can run under without breakage, because every source you do not allow is one an attacker cannot use.

Go deeper

Want to know whether your CSP and other security headers actually protect you? Analyse any site with StackOptic — security, performance and SEO in one free report.

Frequently asked questions

What is a Content Security Policy?

A Content Security Policy (CSP) is an HTTP response header that lets a website control which sources of content the browser is allowed to load and execute. By listing the origins permitted to supply scripts, styles, images, fonts and other resources, it gives the browser a strict rule set. Its main purpose is to mitigate cross-site scripting: if an attacker injects a script from an origin not on the allow-list, the browser refuses to run it.

How does CSP prevent cross-site scripting (XSS)?

Cross-site scripting works by getting the browser to execute attacker-supplied JavaScript. A CSP defeats this by telling the browser to only run scripts from explicitly trusted sources — specific origins, or scripts carrying a matching nonce or hash. An injected inline script or one loaded from an unapproved domain does not match the policy, so the browser blocks it. CSP does not fix the underlying injection flaw, but it stops the injected code from running.

What is the difference between a CSP nonce and a hash?

Both let you allow specific inline scripts without opening up all inline script. A nonce is a random value generated per page load, added to the CSP header and to each trusted script tag; the browser runs only scripts whose nonce matches. A hash is a cryptographic digest of an exact script's contents listed in the policy; the browser runs only inline scripts whose contents hash to that value. Nonces suit dynamic pages; hashes suit static, unchanging scripts.

What does Content-Security-Policy-Report-Only do?

Report-Only mode applies your policy without enforcing it: the browser does not block anything, but it reports every action that would have been blocked to a reporting endpoint you specify. This lets you deploy a candidate policy on a live site, observe what it would break, and refine it safely before switching to enforcement. It is the recommended first step for any CSP rollout, because a strict policy enforced blind will almost always break legitimate functionality.

Why is 'unsafe-inline' a problem in a CSP?

'unsafe-inline' tells the browser to allow any inline script or style, including ones an attacker injects — which removes most of the protection CSP is meant to provide. A policy with 'unsafe-inline' in script-src looks like a CSP but offers little real XSS defence. The secure alternative is to allow specific inline scripts via a nonce or hash, often combined with 'strict-dynamic', so that only your intended inline code runs and injected code does not.

Analyse any website with StackOptic

Get the full technology stack, performance, security and SEO report in seconds — free.

Analyse a website

Related articles