Content-Security-Policy: A Developer's Step-by-Step Setup Guide
CSP is the most powerful security header and the easiest to get wrong. Follow this staged rollout to deploy it without breaking your site.
Content-Security-Policy (CSP) is the single most effective defence against cross-site scripting, and also the header most likely to break your site if you deploy it carelessly. The secret is not to write a perfect policy on the first try — it is to roll it out in stages, watching what would break before you let it break anything. This guide walks through that staged rollout end to end.
How CSP works in one paragraph
CSP is an allow-list for resources. You declare where scripts, styles, images, fonts and other content are permitted to come from, and the browser refuses to load anything outside that list. If an attacker injects a script tag, it will not run unless its source is on your allow-list — which is why CSP neutralises most XSS attacks.
Step 1: Inventory what your site loads
Before writing a single directive, open your site with the browser dev tools on the Network tab and list every external origin it talks to: your CDN, analytics, fonts, embedded videos, payment widgets. This inventory becomes your allow-list. Skipping this step is the number one reason CSP rollouts break things.
Step 2: Deploy in report-only mode
Never start with an enforcing policy. Start with Content-Security-Policy-Report-Only, which tells the browser to report violations without blocking anything. Your site keeps working exactly as before, but you collect a list of everything your real policy would have blocked.
add_header Content-Security-Policy-Report-Only "default-src 'self'; report-uri /csp-report" always;
It lets you see every resource your policy would block, on real traffic, with zero risk to visitors. Leave it running for at least a week before enforcing.
Step 3: Refine the policy
Read the violation reports and widen your policy to allow the legitimate sources you missed in step one. Aim to be as specific as possible — list exact domains rather than wildcards, and avoid 'unsafe-inline' for scripts wherever you can, since it re-opens the XSS door CSP is meant to close. If you have inline scripts, use a nonce or hash instead.
default-src 'self'; script-src 'self' https://cdn.example.com; style-src 'self' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; object-src 'none'; frame-ancestors 'none';
Step 4: Switch to enforcing
Once the reports are quiet and your real resources all load, swap the header name from Content-Security-Policy-Report-Only to Content-Security-Policy. The exact same policy now blocks anything outside your allow-list. Keep a report-uri or report-to directive in place so you continue to hear about violations after enforcement.
Common pitfalls
- Using `'unsafe-inline'` for scripts — it defeats the purpose; use nonces or hashes instead.
- Forgetting third-party widgets — payment, chat and video embeds each need their origins allowed.
- Wildcards everywhere —
*allow-lists undermine the protection; be specific. - Enforcing on day one — always go through report-only first.
After enforcing, run a scan to confirm your CSP is present, valid and free of risky directives — and to catch a regression if a future deploy weakens it.
CSP rewards patience. Inventory your resources, watch in report-only mode, refine until the reports are clean, then enforce — and you will get the full XSS protection of a strict policy without the broken-page horror stories. Deploy it once, properly, and it quietly protects every visitor on every page from then on.
