I scanned over 10 live sites last week benchmarking security headers. Eight of them had no Content-Security-Policy header at all — including a few that scored well on every other check. CSP is the one header developers consistently skip because it's the only one that can actually break your site if you get it wrong.
This guide shows you how to add it safely — without breaking anything.
TL;DR
CSP tells browsers which scripts, styles and resources are allowed to load. Missing it means an XSS payload can run freely. Always deploy inreport-onlymode first, audit violations in DevTools, then enforce. Fix instructions for Cloudflare, Nginx and Apache are below.
What CSP actually does
Content-Security-Policy is a response header that gives browsers an allowlist of resources. Anything not on the list gets blocked before it executes.
The primary threat it limits is XSS. Without CSP, an injected script runs with full access to your page — it can steal session cookies, log keystrokes or redirect users. With CSP, that same script hits the allowlist and gets blocked.
It also covers two other misconfigurations:
Clickjacking. The frame-ancestors directive controls which domains can embed your page in an <iframe> — the modern replacement for X-Frame-Options.
Mixed content. The upgrade-insecure-requests directive tells browsers to automatically upgrade HTTP resource requests to HTTPS.
Why most developers skip it
CSP is the only security header where a wrong value actively breaks your site for real users. Miss a CDN domain in script-src and your analytics stops loading. Get font-src wrong and your Google Fonts disappear. This is why devs avoid it — and why the report-only approach below makes it safe.
Step 1 — Always start in report-only mode
This is the rule that makes CSP deployable without stress.
Content-Security-Policy-Report-Only applies your policy in monitoring mode only. The browser enforces nothing but logs every resource that would have been blocked to the DevTools console. You see all your gaps before a single user is affected.
The workflow:
- Deploy the report-only header with a starting policy
- Browse your site — homepage, login, checkout, any page with third-party scripts
- Open DevTools → Console, look for CSP violation messages
- Add the missing sources to your policy
- Repeat until the console is clean
- Switch
Content-Security-Policy-Report-OnlytoContent-Security-Policy
Don't skip this. The more third-party scripts your site loads, the more violations you'll find.
Cloudflare
Cloudflare Pages — _headers file:
/*
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self'; frame-ancestors 'self'
After auditing, enforce:
/*
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted-cdn.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com; connect-src 'self' https://api.analytics.com; frame-ancestors 'self'
Cloudflare proxy — Transform Rules:
Dashboard → Rules → Transform Rules → Modify Response Header. Add Content-Security-Policy as a new header, set it to match all requests.
Nginx
server {
listen 443 ssl http2;
server_name example.com;
# Step 1: report-only — audit what your site loads
add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self'; frame-ancestors 'self'" always;
# ... rest of your config
}
Once violations are clear, enforce:
server {
listen 443 ssl http2;
server_name example.com;
# Step 2: enforce after testing
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://trusted-cdn.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 'self'" always;
# ... rest of your config
}
The always keyword sends the header on error responses too — don't leave it out. Test and reload:
nginx -t
systemctl reload nginx
Apache
# a2enmod headers (if not already enabled)
<VirtualHost *:443>
ServerName example.com
# Step 1: report-only to audit
Header always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self'; frame-ancestors 'self'"
</VirtualHost>
Enforce after testing:
<VirtualHost *:443>
ServerName example.com
# Step 2: enforce
Header always set Content-Security-Policy "default-src 'self'; script-src 'self' https://trusted-cdn.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 'self'"
</VirtualHost>
.htaccess (shared hosting):
<IfModule mod_headers.c>
Header always set Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; frame-ancestors 'self'"
</IfModule>
systemctl restart apache2
The directives that matter most
A realistic starting policy:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' https:; connect-src 'self'; frame-ancestors 'self'
default-src 'self' — catch-all fallback for any directive you don't explicitly set. Always include this.
script-src 'self' — the directive that actually limits XSS impact. To allow external scripts, add their origin explicitly. For inline scripts, use nonces ('nonce-{random}') or hashes ('sha256-{hash}') instead of 'unsafe-inline'.
style-src 'self' 'unsafe-inline' — 'unsafe-inline' is often needed for frameworks that inject inline styles. Risk from inline styles is much lower than from inline scripts — acceptable in most cases.
img-src 'self' data: https: — allows images from your origin, data URIs and any HTTPS source.
font-src 'self' https: — if you're on Google Fonts, add https://fonts.gstatic.com.
connect-src 'self' — covers fetch, XHR and WebSocket. Add any external API your frontend calls.
frame-ancestors 'self' — prevents your site being embedded in iframes. Use 'none' if you never need to be framed.
Mistakes that will ruin your day
Enforcing without auditing first. I cannot stress this enough. One missing CDN domain and your payment widget stops loading in production.
'unsafe-inline' in script-src. Negates most XSS protection. An injected script is inline — it passes the check and runs freely.
'unsafe-eval' in script-src. Allows eval() and new Function(). Most modern libraries don't need it — check before adding.
script-src * (wildcard). Allows scripts from any origin. A compromised CDN anywhere on the internet can now inject code into your page.
'self' doesn't cover subdomains. https://static.example.com and https://api.example.com need to be listed explicitly.
Verifying it works
DevTools — quickest check:
F12 → Network tab → click the main document → Response Headers. Look for content-security-policy.
While in report-only mode, violations appear in the Console like this:
Refused to load the script 'https://cdn.example.com/lib.js' because it violates the following Content Security Policy directive: "script-src 'self'"
Each line tells you exactly what to add to your policy.
curl:
curl -I https://yoursite.com
CSP is the most powerful security header you can add — and the most skipped because of deployment risk. The report-only approach removes that risk entirely. Audit first, enforce second, never the other way around.
If you want to see how your site scores across CSP and the other security headers, Guardr scans any domain for free — no signup required.
What's the trickiest third-party script you've had to whitelist in your CSP? Drop it in the comments — curious what everyone's running into.
Top comments (0)