Content-Security-Policy explained: directives, unsafe-inline, nonces and a safe rollout
CSP is the most powerful security header and the easiest to get wrong. The directives that matter, how nonces and hashes replace 'unsafe-inline', and a rollout plan that won't break your site.
A Content-Security-Policy (CSP) is a response header that tells the browser where a page may load scripts, styles, images, frames and other resources from. Its main job is damage control: if an attacker manages to inject a <script> into your page through a cross-site scripting (XSS) bug, a good policy stops the browser from running it.
The catch is that "a CSP" and "a good CSP" are very different things. A header full of wildcards passes a presence check and protects almost nothing. This guide explains how to tell the difference.
How a policy is built
A policy is a list of directives separated by semicolons. Each directive names a resource type and the sources allowed for it: Content-Security-Policy: default-src 'self'; img-src 'self' https://images.example-cdn.com; object-src 'none'.
Sources can be 'self' (your own origin), specific origins such as https://js.stripe.com, schemes such as https: or data:, keywords like 'none', or nonces and hashes, explained below. Keywords are written in single quotes; origins aren't.
The directives that matter most
default-src: the fallback for most resource types you don't list separately.script-src: where scripts may come from. This is the directive that actually defends against XSS, so it deserves the most care.style-src,img-src,font-src,connect-src(fetch, XHR, WebSockets),frame-srcandmedia-src: the other resource types.object-src 'none': blocks plugins such as<object>and<embed>, an old but still-cited route for script execution.base-uri 'self'or'none': stops an injected<base>tag from changing where relative script URLs point.frame-ancestors: which sites may embed your page in a frame — the modern replacement for X-Frame-Options.form-action: where forms may submit to.upgrade-insecure-requests: asks the browser to loadhttp://subresources over HTTPS.report-to/report-uri: where the browser sends violation reports.
What weakens a policy
'unsafe-inline'
In script-src, 'unsafe-inline' allows every inline <script> block and inline event handler such as onclick. That's exactly what an XSS attack injects, so a policy with 'unsafe-inline' scripts barely slows it down. Browsers that support nonces or hashes ignore 'unsafe-inline' when one is present, which is why it's sometimes kept as a fallback for very old browsers alongside a nonce.
'unsafe-eval'
'unsafe-eval' allows eval(), new Function() and string arguments to setTimeout. It turns data into code, which is risky. Some older libraries and template engines need it; modern builds usually don't.
Broad sources
*, https:, http: or data: in script-src allow scripts from practically anywhere, so an attacker can host their script on any HTTPS server. Popular CDNs are a subtler problem: allowing an entire public CDN host lets an attacker load any library hosted there, including old versions with known bypasses.
Report-Only on its own
Content-Security-Policy-Report-Only reports violations but blocks nothing. It's the right way to start, and the wrong place to stop.
Nonces and hashes: the way out of 'unsafe-inline'
A nonce is a random value your server generates for every response. You put it in the header — script-src 'nonce-R4nd0mV4lue' — and on each script tag you trust: <script nonce="R4nd0mV4lue">. The browser runs only scripts carrying the matching nonce. An injected script doesn't know the value, so it's blocked. The nonce must be unpredictable and different on every response; a fixed nonce is no better than 'unsafe-inline'.
A hash allows one specific inline script by the SHA-256, SHA-384 or SHA-512 hash of its contents: script-src 'sha256-…'. Hashes suit static pages where a nonce per response isn't practical; any change to the script changes the hash.
Adding 'strict-dynamic' lets scripts you trusted with a nonce or hash load further scripts, and tells supporting browsers to ignore host allowlists. That makes a so-called strict CSP practical even for sites with tag managers and widgets: script-src 'nonce-{random}' 'strict-dynamic'; object-src 'none'; base-uri 'none'.
See how your CSP reads
Rudra's free security checker parses your Content-Security-Policy directive by directive and flags 'unsafe-inline', 'unsafe-eval', broad script sources and missing object-src or base-uri.
A rollout plan that won't break your site
- Inventory what the page loads. Open developer tools on your key pages — home, login, checkout, pages with embeds — and note every script, style, font, frame and API origin.
- Send a Report-Only policy. Start with something like
default-src 'self'; script-src 'self' 'nonce-{random}'; object-src 'none'; base-uri 'self'inContent-Security-Policy-Report-Only. Violations show up in the browser console, and at your reporting endpoint if you set one. - Fix or allow each violation. Move inline scripts to files or give them a nonce, replace inline
onclickhandlers withaddEventListener, and add the specific third-party origins you actually use. - Enforce. Once reports are quiet across your key journeys, switch the header name to
Content-Security-Policy. You can keep sending a stricter Report-Only policy alongside to test the next step. - Keep it maintained. Every new widget, analytics tool or payment provider needs a policy change. Re-check after adding one.
Set the header on the server, CDN or framework, not in a <meta> tag if you can avoid it: a meta-tag policy can't use frame-ancestors, reporting or Report-Only mode.
What Rudra checks in your policy
The free website security checker reads the CSP your page actually sends and shows the parsed directives as evidence. It flags a missing policy, a policy sent only as Report-Only, a policy that doesn't restrict scripts at all (no script-src or default-src), 'unsafe-inline' in the script policy (not flagged when a nonce or hash is present), 'unsafe-eval', broad script sources such as * or https: (unless you use 'strict-dynamic' with a nonce or hash), and — as informational notes — missing object-src 'none', missing base-uri and inline styles. It recognises frame-ancestors as clickjacking protection when it isn't * or a bare scheme.
A checker can tell you what your policy allows. It can't tell you whether your pages still work under it — only testing your real journeys in report-only mode can.
Frequently asked questions
Does a Content-Security-Policy prevent XSS?
It doesn't fix the underlying bug, but a strict policy stops most injected scripts from running, which limits the damage. You still need to escape output and validate input.
Is 'unsafe-inline' in style-src a problem?
It's much less serious than in script-src. Injected styles can still be abused in some attacks, so removing it is good hardening, but prioritise the script policy first.
Can I use the same nonce on every page?
No. A nonce must be random and unique for every response. A fixed or reused nonce can be copied by an attacker and gives no protection.
How long should I run Report-Only before enforcing?
Long enough to cover your real traffic patterns — often one to two weeks — including login, checkout and any pages with third-party embeds. Enforce once the reports only show noise such as browser extensions.