How to Configure Content Security Policy (CSP)

A step-by-step guide to implementing CSP safely — starting with report-only mode, identifying violations, building your policy, and deploying it on common web server platforms.


Content Security Policy (CSP) is a powerful browser security feature that prevents cross-site scripting (XSS) and data injection attacks. Configuring it incorrectly can break your website. This guide walks through the safe, methodical process of deploying CSP — starting with monitoring mode and building up to full enforcement.

The Safe CSP Deployment Process

The biggest mistake developers make with CSP is jumping straight to enforcement. Instead, follow this process:

  1. Deploy report-only mode to collect violations without breaking anything.
  2. Analyze violations to discover all resource sources your site uses.
  3. Build a policy that permits legitimate sources and blocks everything else.
  4. Switch to enforcement mode with the finalized policy.
  5. Monitor ongoing violations and refine the policy.

Step 1: Deploy CSP in Report-Only Mode

Start by adding the Content-Security-Policy-Report-Only header with a restrictive policy and a reporting endpoint. This records violations without blocking anything:

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

You need an endpoint to receive reports. A simple Node.js handler:

javascript
// Express.js CSP report endpoint app.post('/csp-report', express.json({ type: 'application/csp-report' }), (req, res) => { console.log('CSP Violation:', JSON.stringify(req.body, null, 2)); res.status(204).end(); });

Alternatively, use a hosted service like Report URI (report-uri.com) which aggregates and visualizes CSP reports without needing your own endpoint.

Step 2: Analyze Violations

After running in report-only mode for a few days, review the violations. Each report looks like:

json
{ "csp-report": { "document-uri": "https://example.com/page", "violated-directive": "script-src", "blocked-uri": "https://cdn.analytics.com/tracker.js", "original-policy": "default-src 'self'; report-uri /csp-report" } }

For each violation, decide:

  • Is this a legitimate resource my site needs? → Add it to the appropriate directive.
  • Is this an injected script from an attacker or browser extension? → Leave it blocked.
  • Is this an inline script or style? → Refactor to external files, or use nonces/hashes.

Step 3: Build Your Policy

Build the policy incrementally based on your violation reports. A typical policy for a site using Google Analytics and Google Fonts:

http
Content-Security-Policy: default-src 'self'; script-src 'self' https://www.googletagmanager.com https://www.google-analytics.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https://www.google-analytics.com; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://www.google-analytics.com; frame-ancestors 'none'; base-uri 'self'; object-src 'none';

Deploying CSP on Nginx

nginx
server { listen 443 ssl; server_name example.com; # Start with report-only for testing add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none'; report-uri /csp-report" always; # When ready, switch to enforcement: # add_header Content-Security-Policy "default-src 'self'; ..." always; }

Deploying CSP on Apache

apache
<VirtualHost *:443> ServerName example.com # Report-only mode Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self' https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; connect-src 'self'; frame-ancestors 'none'; report-uri /csp-report" # Enforce after testing (replace the above with): # Header always set Content-Security-Policy "default-src 'self'; ..." </VirtualHost>

Deploying CSP in Next.js

Next.js supports CSP with nonces for secure inline script handling:

javascript
// middleware.js (Next.js 13+) import { NextResponse } from 'next/server'; import crypto from 'crypto'; export function middleware(request) { const nonce = Buffer.from(crypto.randomUUID()).toString('base64'); const cspHeader = [ "default-src 'self'", `script-src 'self' 'nonce-${nonce}' 'strict-dynamic'`, `style-src 'self' 'nonce-${nonce}'`, "img-src 'self' blob: data: https:", "font-src 'self'", "connect-src 'self' https://api.example.com", "frame-ancestors 'none'", "object-src 'none'", ].join('; '); const response = NextResponse.next({ request: { headers: new Headers(request.headers) }, }); response.headers.set('Content-Security-Policy', cspHeader); response.headers.set('x-nonce', nonce); return response; }

Deploying CSP on Cloudflare

Use Cloudflare Transform Rules to add CSP headers:

  1. Go to RulesTransform Rules in the Cloudflare dashboard.
  2. Create a new Modify Response Headers rule.
  3. Set the header name to Content-Security-Policy and add your policy value.
  4. Set the rule to apply to your hostname.
Check your CSP headerUse the ShowDNS CSP Checker to verify your Content-Security-Policy header is correctly set and identify common misconfigurations.

Handling Inline Scripts with Nonces

If you cannot move all scripts to external files, use nonces instead of 'unsafe-inline':

html
<!-- In your HTML, reference the nonce --> <script nonce="BASE64_NONCE_HERE"> // This inline script is allowed console.log('Allowed by nonce'); </script> <!-- In your CSP header --> <!-- script-src 'nonce-BASE64_NONCE_HERE' --> <!-- A new nonce must be generated per request -->

Frequently Asked Questions

How long should I run in report-only mode?

Run report-only mode for at least one week to capture violations from all page types and user flows. High-traffic sites may need less time; low-traffic sites should run longer to capture all scenarios.

Why am I still seeing violations after fixing my CSP?

Browser extensions inject scripts and styles that can generate CSP violations even for a correctly configured site. Filter out extension-related violations (they typically come from chrome-extension:// or moz-extension:// URIs).

Does CSP work the same way in all browsers?

CSP Level 2 is supported in all modern browsers. Some newer directives (like report-to) have variable support. Stick to well-supported directives unless you have confirmed browser support for newer ones.

Related Articles