Website Spec
SecurityRecommendedUpdated

Content Security Policy (CSP)

A CSP tells browsers which sources of script, style, image, and frame content to trust. A good policy stops most XSS and data-exfiltration attacks dead.

What it is

Content Security Policy is a response header that restricts which resources a page may load and execute. Level 3 is the current specification. The browser enforces the policy; any script, style, image, frame, or fetch from a source the policy does not allow is blocked, and a report is sent if a reporting endpoint is configured.

Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-r4nd0m' 'strict-dynamic'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'; report-to csp-endpoint

Why it matters

The single biggest class of web vulnerabilities is cross-site scripting. A solid CSP turns a successful XSS injection from “attacker runs JavaScript in your origin” into “browser blocks the script and logs a violation”. CSP also limits where data can be exfiltrated to, prevents your site from being framed, and disables dangerous legacy features like inline event handlers.

How to implement

Build a strict, nonce-based policy. The recommended pattern from Google’s strict CSP guidance:

Content-Security-Policy:
  default-src 'self';
  script-src 'nonce-{random}' 'strict-dynamic' https: 'unsafe-inline';
  object-src 'none';
  base-uri 'none';
  frame-ancestors 'none';
  require-trusted-types-for 'script';
  report-to csp-endpoint

That script-src looks self-contradicting: it lists 'unsafe-inline' and https:, the very things the mistakes below tell you to drop. The contradiction is deliberate and safe. Once 'strict-dynamic' and a nonce are present, a modern browser ignores both. The CSP Level 3 rule is that 'strict-dynamic' discards every host allowlist and 'unsafe-inline', leaving the nonce as the only thing that decides what runs. The https: and 'unsafe-inline' are there purely as a fallback for older browsers that predate nonces, giving them a weak policy rather than none. The nonce carries the real policy; the leftovers are for readers that cannot understand it. What you must never do is ship 'unsafe-inline' without a nonce and 'strict-dynamic': on its own it is not a fallback, it is the whole policy, and it permits exactly the injected inline script CSP exists to block.

Key directives:

  • default-src 'self' — fallback for all fetch directives. Allow your own origin by default.
  • script-src — controls JavaScript. Use a per-response nonce; the strict-dynamic keyword lets a trusted script load further trusted scripts.
  • style-src — controls CSS. Same nonce model where possible.
  • img-src, font-src, connect-src, media-src — list the third parties you actually use.
  • frame-ancestors 'none' — replaces X-Frame-Options. See Clickjacking protection.
  • object-src 'none' — kills Flash and plugin embeds.
  • base-uri 'none' — blocks <base> tag injection attacks.
  • report-to — endpoint that receives violation reports as JSON.

Generate a fresh nonce per response and embed it in every inline <script> tag.

Common mistakes

  • unsafe-inline or unsafe-eval standing alone. As above, 'unsafe-inline' only degrades safely with a nonce and 'strict-dynamic' beside it; by itself it neutralises the protection, and 'unsafe-eval' re-enables eval(). Use nonces or hashes.
  • Wildcards like script-src * or https: alone. Almost as bad as no policy at all.
  • No frame-ancestors. Leaves clickjacking open.
  • Forgetting the nonce on server-rendered inline scripts. The browser will block them and the page breaks.
  • Shipping a report-only policy forever. Use Content-Security-Policy-Report-Only to test, then switch to enforcing.

Verification

  • curl -sI https://example.com | grep -i content-security-policy should return the header.
  • Run the page through Google CSP Evaluator.
  • Open DevTools → Console. CSP violations appear as Refused to load … errors.
  • Wire up a reporting endpoint and watch for unexpected blocks before tightening.

Related topics

Sources & further reading