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:
- Deploy report-only mode to collect violations without breaking anything.
- Analyze violations to discover all resource sources your site uses.
- Build a policy that permits legitimate sources and blocks everything else.
- Switch to enforcement mode with the finalized policy.
- 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:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-reportYou need an endpoint to receive reports. A simple Node.js handler:
// 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:
{
"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:
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
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
<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:
// 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:
- Go to Rules → Transform Rules in the Cloudflare dashboard.
- Create a new Modify Response Headers rule.
- Set the header name to
Content-Security-Policyand add your policy value. - Set the rule to apply to your hostname.
Handling Inline Scripts with Nonces
If you cannot move all scripts to external files, use nonces instead of 'unsafe-inline':
<!-- 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.