Content Security Policy (CSP): A Complete Guide

How CSP prevents cross-site scripting, data injection, and clickjacking -- and how to deploy it without breaking your site.

What CSP does and why it matters

Content Security Policy is a browser security mechanism that restricts which resources a page can load and execute. It is delivered as an HTTP response header (Content-Security-Policy) and acts as a whitelist: the browser blocks anything not explicitly allowed.

Without CSP, any script injected into your page (via XSS, a compromised third-party library, or a man-in-the-middle attack) executes with the full privileges of your origin. CSP limits the blast radius of injection attacks by preventing the browser from executing inline scripts, loading scripts from unauthorized origins, or submitting form data to attacker-controlled endpoints.

CSP also mitigates clickjacking (via frame-ancestors), mixed content (by upgrading insecure requests), and data exfiltration (by restricting connect-src and form-action).

Directives reference

Each directive controls a specific resource type. If a directive is not set, the browser falls back to default-src. If default-src is also absent, the resource is allowed from any origin.

Common mistakes

'unsafe-inline'

Adding 'unsafe-inline' to script-src disables the primary XSS protection that CSP provides. Any injected <script> tag will execute. If you need inline scripts, use a per-request nonce:

Content-Security-Policy: script-src 'nonce-abc123def456'

<script nonce="abc123def456">
  // This script runs. Injected scripts without the nonce do not.
</script>

The nonce must be cryptographically random and regenerated on every response. A static nonce is equivalent to 'unsafe-inline'.

'unsafe-eval'

Permits eval(), Function(), setTimeout('string'), and similar dynamic code execution. This is often required by older template engines and some frameworks. Audit your dependencies and replace eval usage where possible. CSP Level 3 introduces 'unsafe-eval' alternatives like 'wasm-unsafe-eval' for WebAssembly without opening the door to full eval.

Overly broad sources

Whitelisting an entire CDN origin like https://cdn.example.com means any file on that CDN can be loaded as a script. If the CDN hosts user-uploaded content, an attacker can upload a malicious JS file and exploit your whitelist. Prefer specific paths or use strict-dynamic with nonces.

report-uri vs report-to

CSP can send violation reports to a URL you control, which is invaluable for debugging and monitoring.

report-uri (CSP Level 2) sends a JSON POST to a specified endpoint when a violation occurs:

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

report-to (CSP Level 3) uses the Reporting API, which is more flexible and supports batching:

Report-To: {"group":"csp","max_age":86400,"endpoints":[{"url":"/csp-report"}]}
Content-Security-Policy: default-src 'self'; report-to csp

Browser support for report-to is still inconsistent. For maximum coverage, include both directives. They do not conflict.

CSP Levels: 1, 2, and 3

CSP Level 1 introduced the basic directive model and source lists. Still widely supported.

CSP Level 2 added nonces, hashes, base-uri, form-action, frame-ancestors, and report-uri. This is the baseline you should target.

CSP Level 3 added strict-dynamic (trust propagation from nonced scripts to scripts they load), report-to, worker-src, and navigate-to. strict-dynamic is the recommended approach for complex applications because it reduces whitelist maintenance -- you only need to nonce the entry-point scripts, and they can dynamically load additional scripts.

Practical examples

Starter policy

Begin with a restrictive baseline in report-only mode to discover what your site actually needs:

Content-Security-Policy-Report-Only:
  default-src 'self';
  script-src 'self';
  style-src 'self';
  img-src 'self';
  font-src 'self';
  connect-src 'self';
  frame-ancestors 'none';
  object-src 'none';
  base-uri 'self';
  report-uri /csp-report

Deploy this header and monitor your report endpoint. Every blocked resource generates a report with the violated directive, the blocked URI, and the document URL. This tells you exactly what to whitelist.

Iterative tightening

After reviewing reports, add the specific origins your site needs:

Content-Security-Policy:
  default-src 'self';
  script-src 'self' 'nonce-{random}';
  style-src 'self' 'nonce-{random}';
  img-src 'self' data:;
  font-src 'self';
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  object-src 'none';
  base-uri 'self';
  form-action 'self';
  upgrade-insecure-requests;
  report-uri /csp-report

Key points: each inline script and style tag gets a server-generated nonce. upgrade-insecure-requests rewrites HTTP subresource URLs to HTTPS automatically. form-action prevents form submissions to third-party origins.

Nginx configuration

# /etc/nginx/snippets/csp.conf
add_header Content-Security-Policy
  "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests"
  always;

Apache configuration

# .htaccess or httpd.conf
Header always set Content-Security-Policy \
  "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'; form-action 'self'; upgrade-insecure-requests"

Traefik middleware

# traefik dynamic config
http:
  middlewares:
    csp:
      headers:
        contentSecurityPolicy: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; font-src 'self'; connect-src 'self'; frame-ancestors 'none'; object-src 'none'; base-uri 'self'"

Check your site's HTTP headers with spectra