Back to Help Center
SECURITY September 11, 2026 · 10 min read

How to Write a Content Security Policy (CSP) That Actually Works

CSP breaks things on first deployment — every unlisted script, style, image, or font is blocked. Report-Only mode is the safety valve. Bake for a week, fix violations, then enforce.

Content Security Policy is the highest-leverage security header — and the most likely to break your site on first deployment. Report-Only + phased rollout is the safe path.

Prerequisites

  • HTTPS deployed (see how to add HSTS safely)
  • Access to your web server / CDN to set response headers
  • An endpoint to receive CSP violation reports (or a service like Sentry, Report URI, DataDome)

Step 1 — Draft a Starting Policy

For a typical web app:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self' https://cdn.example.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  font-src 'self' https://fonts.gstatic.com;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  base-uri 'self';
  form-action 'self';
  upgrade-insecure-requests;
  report-uri /csp-violation-report;

Note the -Report-Only suffix. Nothing is blocked yet.

Directive breakdown:

  • default-src 'self' — fallback for every fetch directive
  • script-src — where scripts can come from
  • style-src — where CSS can come from ('unsafe-inline' needed for inline <style> tags)
  • img-src — where images can come from (data: allows data URLs)
  • font-src — where fonts can come from
  • connect-src — where fetch / XHR / WebSocket can target
  • frame-ancestors 'none' — who can embed you in an iframe (replaces X-Frame-Options)
  • base-uri 'self' — restricts <base> tag targets
  • form-action 'self' — where forms can POST
  • upgrade-insecure-requests — auto-upgrade any HTTP subresource to HTTPS
  • report-uri — where violations are sent

Step 2 — Deploy Report-Only

Ship the Content-Security-Policy-Report-Only header to production. Nothing blocked. Violations sent to your endpoint.

Step 3 — Collect Violations (1 Week)

Every embed not in your policy generates a violation report. Common early findings:

  • Google Analytics — needs https://www.google-analytics.com in script-src and connect-src
  • Google Tag Manager — needs https://www.googletagmanager.com in script-src
  • Stripe checkout — needs https://js.stripe.com in script-src and https://api.stripe.com in connect-src
  • Legacy jQuery on a CDN — add the CDN’s origin
  • Inline <script> tags — add 'nonce-{unique-per-request}' and set the nonce header

For each violation, decide:

  1. Legitimate embed — add its origin to the appropriate directive
  2. Old/unused third-party — remove from your codebase
  3. Malicious injection — investigate; this is what CSP catches

Step 4 — Iterate

Update the Report-Only header. Wait another week. Repeat until violation reports are only from noisy but harmless sources.

Step 5 — Flip to Enforcing

Remove -Report-Only. Now the header enforces:

Content-Security-Policy:
  default-src 'self';
  ...
  report-uri /csp-violation-report;

Keep report-uri — even when enforcing, you want visibility into blocked requests.

Step 6 — Verify

Run the security headers checker — confirm the enforcing CSP is deployed and no critical directive is missing.

Test the site manually — every core user flow should work. Watch violation reports for the first 48 hours; roll back if legitimate features break.

Nonces for Inline Scripts

Rather than 'unsafe-inline' in script-src, use per-request nonces:

<script nonce="abc123xyz">...</script>
Content-Security-Policy: script-src 'self' 'nonce-abc123xyz';

Generate a new nonce per response. Modern frameworks (Next.js, Rails, Django) support this natively.

Common Miss

  • Deploying enforcing CSP without a Report-Only bake period — site breaks silently
  • 'unsafe-inline' in script-src — kills the main XSS protection CSP provides
  • Missing frame-ancestors — clickjacking still possible even with X-Frame-Options
  • No report-uri — flying blind
  • Different CSP on different routes — attackers hit the routes without CSP

Follow up with the CSP glossary entry and the full HTTP security headers guide.

Check the CSP glossary entry, read the HTTP security headers explainer, and see how to add HSTS safely.