Rudra Analyzer

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.

Rudra Techno Team 8 min read

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-src and media-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 load http:// 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.

Checks one page · Free · No sign-up needed

A rollout plan that won't break your site

  1. 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.
  2. Send a Report-Only policy. Start with something like default-src 'self'; script-src 'self' 'nonce-{random}'; object-src 'none'; base-uri 'self' in Content-Security-Policy-Report-Only. Violations show up in the browser console, and at your reporting endpoint if you set one.
  3. Fix or allow each violation. Move inline scripts to files or give them a nonce, replace inline onclick handlers with addEventListener, and add the specific third-party origins you actually use.
  4. 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.
  5. 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.

Keep reading

Find out what's holding your website back

Run a free check on any public page. No sign-up needed for a basic check.