Content-Security-Policy is the most effective defence against cross-site scripting available to you, and the one most likely to break your site the first time you deploy it. Both of those things are true for the same reason: it works by refusing to run anything you have not explicitly allowed.
The CSP Generator will build a policy from the sources you actually use, and the Security Headers Checker shows what you are currently sending.
What CSP is for
CSP does not stop injection. If your application writes unescaped user input into a page, that flaw is still there. What CSP does is limit what the injected content can achieve — the browser simply refuses to execute a script from a source you did not authorise.
Think of it as the second lock. The first lock is escaping output properly. CSP is what saves you the day the first lock fails.
The directives that matter
default-src — the fallback for everything you do not specify. Starting with default-src 'self' means "only load things from my own origin unless I say otherwise", which is the right default.
script-src — the important one. Where JavaScript may come from.
style-src — where CSS may come from.
img-src — images. Usually the loosest, because images are low risk. img-src 'self' data: https: is common and reasonable.
connect-src — where fetch, XHR and WebSockets may connect. Worth tightening: this is the directive that stops injected script exfiltrating data to an attacker's server.
frame-src — what may be embedded in your page. If you embed YouTube, this is where that goes.
frame-ancestors — who may embed you. This is the modern replacement for X-Frame-Options and the anti-clickjacking control.
object-src 'none' — blocks Flash-era plugin content. There is no reason to allow it. Set it to none and forget it.
base-uri 'self' — stops an injected <base> tag rewriting where your relative URLs point. Cheap, and closes a real attack.
The unsafe-inline problem
Here is where most real deployments end up compromised.
If your templates contain inline <script> blocks or onclick="..." attributes, a strict CSP blocks them. The quick fix is to add 'unsafe-inline' to script-src — and at that point you have given away most of the protection, because injected script is inline script.
The honest options:
- Move inline scripts into external files. Tedious, correct, permanent.
- Use a nonce. Generate a random value per response, put it on your legitimate script tags and in the header as
'nonce-abc123'. An injected script has no nonce and is blocked. The nonce must be genuinely random per request — a fixed nonce is worse than none, because it looks like protection. - Use hashes for a small number of static inline blocks that never change.
If you must ship 'unsafe-inline' to get a policy deployed at all, do it — the other directives still have value. But write it down as debt with a date on it, not as a finished job.
Report-only first
This is the part people skip and then regret.
Content-Security-Policy-Report-Only: default-src 'self'; report-uri /csp-report
Sent as Report-Only, the browser enforces nothing. It just tells you what would have been blocked. Run that for a week or two on real traffic and you will discover the analytics tag, the font CDN, the payment widget and the chat script that nobody documented.
Deploying an enforcing policy without this step is how you find out at 2am that checkout is broken for everyone using a particular browser.
A starting policy
For a typical content site with no third-party scripts:
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'self'
Add each third-party origin explicitly as you find it in the report-only run. Resist the urge to add https: as a wildcard to script-src — that allows any HTTPS host on the internet to run code on your page, which is barely a policy at all.
What CSP cannot do
It is a browser instruction, so it only protects browser users. It does nothing for your API consumers, nothing about SQL injection, nothing about broken access control, and nothing about a server-side vulnerability.
It also does nothing if you never fix the injection itself. A policy that blocks an injected script is telling you there is an injected script. That is a finding, not a resolution.
Common questions
Why is my CSP blocking my own scripts?
They are almost certainly inline. Move them to external files, or use a per-request nonce.
Is unsafe-inline ever acceptable?
As a temporary step to get a policy deployed, yes. As a permanent setting it removes most of CSP's value against XSS.
Do I need X-Frame-Options if I have frame-ancestors?
Not for modern browsers, but sending both costs nothing and covers older clients.
How do I test a policy safely?
Send it as Content-Security-Policy-Report-Only first. Nothing is blocked; you just collect reports on what would have been.
Where to go next
- Build a policy with the CSP Generator, then confirm what you are sending with the Security Headers Checker.
- A permissive CORS policy can undo careful CSP work — worth checking both together.
- Policy breaking something and you cannot see why? Paste it in the BitCops community.
- XSS and its defences are covered in the free BitCops courses.
- Related: the rest of the security headers and the cookie flags CSP does not cover.

Comments
No comments yet. Be the first to add one.
Leave a comment
Your email is required so we can reply, and is never published or shared. Comments are reviewed before they appear.