Content Security Policy (CSP): Setup, Bypass Risks, and Deployment
1. What CSP Actually Blocks
CSP delivers rules through an HTTP response header, and the browser uses them to decide which resources can load. It mainly guards against two problems: malicious scripts injected via XSS, and unauthorized third-party domains embedded in the page.
It is not a firewall. CSP cannot stop SQL injection, nor can it stop a server from being compromised. It governs resource loading on the browser side. It works alongside HTTPS and HSTS. For HTTPS setup, see HTTPS Setup for Your Own Site.
A common pain point for independent sites is too many third-party scripts: GA4, GTM, Hotjar, live chat, ad pixels. Every script you add means another domain on the CSP allowlist, or the browser blocks it. For third-party script governance, see Third-Party Script Governance for Independent Sites.
One often overlooked fact: CSP has almost no negative impact on SEO. Googlebot itself does not enforce resource restrictions beyond JS, so a properly configured CSP will not affect indexing. Worrying that CSP hurts crawling is unnecessary.
| Directive | What it governs | Recommended value |
|---|---|---|
| default-src | Fallback for all resources | ‘self’ |
| script-src | JS script sources | ‘self’ + analytics domains |
| style-src | CSS sources | ‘self’ ‘unsafe-inline’ |
| img-src | Image sources | ‘self’ data: + CDN |
| object-src | Plugins | ‘none’ (must be disabled) |
| frame-ancestors | Who can embed this site | ‘none’ |
2. Three-Step Deployment: Report, Tighten, Enforce
The most common mistake is going straight to the Content-Security-Policy header. That immediately blocks every script on the site not on the allowlist, taking down GA4, live chat, and pixels all at once, and the business team will complain right away.
The right approach is to start with the Content-Security-Policy-Report-Only header. The browser only reports violations without blocking, so you collect two weeks of logs and add every domain actually in use to the allowlist.
Example scenario 1: adding CSP to a WordPress site, start with Report-Only.
Two days later, the report shows three domains: googletagmanager.com, hotjar.com, and youtube.com.
Add them one by one to script-src and frame-src. After two weeks, the report shows only single digits per day, so switch to enforce mode.
| Stage | Response header | Action | Duration |
|---|---|---|---|
| Step one | Content-Security-Policy-Report-Only | Report only, don’t block | 14 days |
| Step two | Same as above, add to the allowlist gradually | Violations drop day by day | Until you’re down to single digits |
| Step three | Content-Security-Policy | Actually blocking | Long term |
| Throughout | Add report-to or report-uri | Collect violation reports | Leave it on |
Inline scripts are the biggest trap. WordPress themes and many plugins write <script> directly into the page, and CSP blocks inline by default. Two ways around it: pull the inline script out into an external .js file, or allow that specific block with a sha256 hash instead of taking the easy route and turning on unsafe-inline.
style-src can be looser than script-src. The Gutenberg editor and many themes output inline styles, so leaving unsafe-inline on style-src temporarily is not a big deal.
But script-src must never allow unsafe-inline. That is the core defense of CSP against XSS. Treat the two directives with different levels of strictness.
3. Bypass risks: don’t let CSP become a dead letter
A misconfigured CSP is as good as no CSP. These three mistakes will take the wall down entirely.
The first is turning on unsafe-inline. It lets any inline script run, including malicious scripts injected by XSS, and the core defense of CSP is broken. The second is using the wildcard *, which means no source restriction at all. The third is leaving out object-src, which lets older browsers bypass CSP through Flash or plugins.
Example scenario 2: taking the easy route and writing ‘unsafe-inline’ in script-src.
Two months later an inline phishing script gets injected into the site, and CSP doesn’t stop it at all.
The right approach is a nonce: generate a one-time random value per request and only allow inline scripts carrying it.
| Wrong approach | Risk | Alternative |
|---|---|---|
| script-src * | Any third-party script can load | List allowlisted domains one by one |
| script-src ‘unsafe-inline’ | XSS-injected scripts still run | Extract to external files or use nonce/hash |
| Omitting object-src | Old plugins can be abused | object-src ‘none’ |
| Not configuring frame-ancestors | Pages get nested in malicious iframes | frame-ancestors ‘none’ |
CSP and compliance are two different things. GDPR cookie consent is a separate mechanism, see GDPR Compliance for Your Own Site. CSP guards against script injection; it does not solve cookie consent. For server-level login and file permissions, see Security Hardening for Your Own Site.
Nonce or hash? Use a hash for fixed inline scripts and a nonce for dynamically generated ones. A fixed tracking snippet in a WordPress theme gets a hash; a script block whose content changes on every request gets a nonce.
Where you deploy it matters too. The CSP response header is best sent from Nginx or the CDN layer, not output by the PHP application. Application-layer output is lost when PHP fails; CDN-layer delivery stays up.
| Deployment location | Reliability | Use cases |
|---|---|---|
| Nginx config | High, survives app failure | Self-hosted server |
| CDN rules | High, takes effect globally | If you use Cloudflare or similar |
| PHP application output | Low, lost on failure | Temporary testing |
The report endpoint also needs its own setup. The address report-uri points to must not be blocked by CSP itself, or the reports won’t get through.
For a standalone site, you can use an off-the-shelf reporting service, or set up a dedicated log endpoint in Nginx to receive reports and write them to a separate log file, keeping them out of your main traffic logs.
Once report volume grows, you need to aggregate. You might get hundreds of reports a day, too many to read one by one, so group them by violated directive and blocked domain, and pay close attention to domains that show up for the first time each day.
A common misconception: people think once CSP is on, they’re all set. In reality, every new third-party script you add and every theme update can introduce new violating sources.
So after switching to Enforce, don’t turn off report-uri. It’s your only signal for catching “a newly added script that someone forgot to whitelist.”
Browser support is worth mentioning too. Modern browsers (recent versions of Chrome, Edge, Firefox, and Safari from the past two years) fully support CSP 3. You can ignore old IE entirely, since visitors to a B2B standalone site basically never use it.
One more deployment detail: the CSP header needs to be sent over both http and https. If you only send it over https, requests during the http origin-fetch stage won’t be protected by CSP.
After switching to Enforce, it’s a good idea to keep a Report-Only header running in parallel for a while, so you both block and report, and if you missed anything you can still see it in the reports.
CSP isn’t a one-time project. Every time you add a new third-party script (support chat, live chat, video, ads), first watch for it in report mode to confirm it shows up as a violation, then add the corresponding domain to the whitelist, and only then switch to production.
Add CSP checks to your new script launch checklist: whitelist the script at the same time you add it, rather than waiting until users report “the widget isn’t showing” after launch to remember CSP.
One rule of thumb: if a third-party script can’t be converted into an external file and its hash can’t be computed, it’s time to reconsider whether that third party is worth using. A third-party service that depends on unsafe-inline is technical debt in itself.
After deployment, spot-check page by page using the Security panel in your browser console. Go through the homepage, product pages, blog pages, and form pages, and confirm there are no red violation warnings.
For pages that fail the spot check, go back to the reports, find the corresponding domain, add it to the whitelist, and observe for another day. Repeat this cycle two or three times and you’re set.
CSP isn’t a security item that’s done once you deploy it; it evolves over the long term alongside your third-party script library. Every time you add a new script, update the whitelist in sync, and never miss whitelisting any new third-party script domain that appears.
| Report source | Meaning | How to handle it |
|---|---|---|
| script-src violation | New script not whitelisted | Add the domain or extract to an external file |
| img-src violation | Image hotlink from a new domain | Add the CDN or switch to a relative path |
| frame-src violation | A new iframe was embedded | Add the video domain or remove it |
| base-uri violation | Page was injected with a base tag | Investigate XSS immediately |
FAQ
Should CSP start in enforce mode from day one?
Inline scripts in my WordPress theme are blocked by CSP. What do I do?
How does CSP relate to HTTPS and HSTS?
Why are all my CSP reports violations?
Can CSP stop SQL injection?
Do I still need to keep report-uri?
Final thoughts
The hard part of CSP isn’t writing the rules, it’s having the patience to run report mode. Two weeks of observation buys you years of script control. That’s a trade worth making.
Three steps to get it done: first, ship Report-Only for 14 days; then add each real domain from the reports to your allowlist one by one and replace unsafe-inline with hashes; once violations drop to single digits, switch to enforce while keeping reporting on for ongoing monitoring.
One line worth remembering: your CSP’s security level depends on how clean your allowlist is. Leave unsafe-inline and wildcards in place and you might as well not have built the wall.
