How to Fix Your Content-Security-Policy (with copy-paste nginx, Apache and Caddy configs)
A Content-Security-Policy (CSP) is the single most effective HTTP header you can set to stop cross-site scripting (XSS) and data-injection attacks. It is also the header people get wrong most often — usually by copying a policy from a blog post, watching half their site break, and then disabling it entirely.
This guide explains how CSP is actually parsed by the browser, walks through the directives that matter, and gives you copy-paste configs for nginx, Apache, Caddy and PHP that you can adapt in about fifteen minutes.
What a CSP actually does
A CSP is an allowlist. The browser reads the header and, for each type of resource — scripts, styles, images, fonts, frames, connections — only loads it if the source is explicitly permitted. Anything not on the list is blocked, and (if you ask for it) reported.
The header is a semicolon-separated list of directives, each naming a resource type and its allowed sources:
Content-Security-Policy: default-src 'self'; script-src 'self' https://cdn.example.com; object-src 'none'
Here default-src 'self' is the fallback for anything not otherwise specified, script-src allows scripts from your own origin plus one CDN, and object-src 'none' blocks <object>/<embed> entirely.
Start in report-only mode
The number-one rule: never ship a new CSP as an enforced policy first. Use the report-only variant, which logs violations without blocking anything:
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Load your whole site, click through every page, and watch the violation reports. When the reports go quiet, promote the exact same policy to the enforcing Content-Security-Policy header.
The directives that matter
default-src— the catch-all fallback. Set it to'self'and then open up only what you need.script-src— the most security-critical directive. This is what actually stops XSS. Avoid'unsafe-inline'if you possibly can (see below).style-src— inline styles are lower-risk than inline scripts, but still worth locking down.img-src,font-src,connect-src— images, web fonts, and the endpoints your JS mayfetch()/XHR/WebSocket to. Analytics and APIs live inconnect-src.frame-ancestors— who may embed you in a frame.frame-ancestors 'none'is the modern replacement forX-Frame-Options: DENY.object-src 'none'andbase-uri 'self'— cheap, high-value hardening almost every site should set.
The inline-script problem
Most breakage comes from inline scripts and styles — <script>…</script> blocks and onclick="…" attributes. CSP blocks these by default. You have three options, from best to worst:
- Move inline JS into external files. Cleanest and most cacheable.
- Use a nonce. Generate a random value per response, add it to the header (
script-src 'nonce-RANDOM') and to each script tag (<script nonce="RANDOM">). This is the right answer when you can't externalise everything. - Use
'unsafe-inline'. This effectively disables CSP's XSS protection for scripts. Treat it as a last resort, and never combine it with a permissivescript-src.
A hash ('sha256-…') is a good middle ground for a small number of fixed inline scripts.
Copy-paste: nginx
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'" always;
The always flag is important — without it nginx omits the header on error responses (4xx/5xx), leaving those pages unprotected.
Copy-paste: Apache
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'"
This requires mod_headers (a2enmod headers on Debian/Ubuntu). Put it in your virtual host or an .htaccess file.
Copy-paste: Caddy
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'"
}
Copy-paste: PHP
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'");
Send this before any output. In Laravel, set it in middleware; in most frameworks, a global response hook is the right place.
Adapt, don't just paste
The policies above assume everything loads from your own origin. In the real world you almost certainly need to add your CDN to script-src/style-src, your analytics endpoint to connect-src, and any embedded fonts to font-src. Add sources one at a time, in report-only mode, until the reports stop.
Keep it from silently regressing
The hard part of CSP isn't writing it once — it's keeping it correct. A new third-party widget, a marketing tag, or a framework upgrade can quietly weaken your policy or break a page. That's exactly what continuous monitoring is for: CompleteStatus grades your security headers (including CSP) A+ to F, tells you precisely what to change, and alerts you the moment the grade drops.
Run a free check right now with the security header checker, or start monitoring free to catch regressions automatically. The free tier includes security-header grading and allows commercial use.