Content Security Policy: Set It Up Safely
A Content Security Policy (CSP) is a response header that tells the browser which scripts, styles, images and frames your pages may load, and the safe way to add one is to run it in report-only mode first, fix what it flags, then switch it to enforce.
This guide is for owners of small sites (WordPress, Shopify, custom sites, indie SaaS) who need to add a CSP header. Maybe a scan flagged it, or a client's security questionnaire asked for it. You'll get a rollout plan, a copy-paste starter policy, and a table that maps common errors to fixes.
What a CSP does (and what it does not)
Your server sends the browser a list of approved sources. If a script comes from somewhere not on the list, the browser refuses to run it. This limits the damage from cross-site scripting (XSS), where an attacker gets their own JavaScript to run on your pages.
A CSP is a safety net, not a repair. It does not fix the bug that let the bad code in. The OWASP Content Security Policy Cheat Sheet describes it as defense in depth, a second layer behind safe coding. It also will not make a site "secure" on its own.
Step 1: Find out what your site loads today
A CSP breaks things when it blocks something you forgot you use. So list your dependencies first.
- Open your site in Chrome or Firefox and press F12 to open DevTools.
- Go to the Network tab and reload the page.
- Note every domain that is not your own: analytics, chat widgets, fonts, payment forms, video embeds, maps.
- Repeat on your most important pages: home, login, checkout, contact form.
You can also run the free security headers checker to see whether you already send a CSP and what else is missing.
Step 2: Start with report-only mode
The header Content-Security-Policy-Report-Only runs your policy in test mode. The browser logs every violation but blocks nothing. This is the single most important habit for a content security policy rollout, because a bad enforced policy can take down your checkout or login.
Here is a strict starter policy to copy:
Content-Security-Policy-Report-Only: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data: https:; font-src 'self'; connect-src 'self'; frame-src 'self'; frame-ancestors 'self'; form-action 'self'; base-uri 'self'; object-src 'none'
What each part means:
default-src 'self'is the fallback: load things only from your own domain.script-src 'self'allows scripts from your domain only. No inline scripts yet.img-src 'self' data: https:allows your images, small embeddeddata:images, and images from any HTTPS site. Images are low risk, so this is a common compromise.frame-ancestors 'self'stops other sites from embedding yours in a frame (a trick called clickjacking).object-src 'none'blocks old plugin content like Flash.base-uri 'self'stops injected<base>tags from redirecting your relative links.
This policy is deliberately strict. Expect violations at first. That is the point.
Step 3: Add the header
Pick the place where you control response headers.
- Nginx: inside your
serverblock, addadd_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; object-src 'none'" always;(use your full policy), then runnginx -tand reload. - Apache: with
mod_headersenabled, addHeader always set Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; object-src 'none'"(use your full policy) to your config or.htaccess. - Cloudflare: go to Rules, then Transform Rules, then Modify Response Header, and set the header there.
- WordPress: a headers plugin such as HTTP Headers can set it without editing server files. Some security plugins and hosts also have a headers screen.
- Shopify: merchants generally cannot add custom response headers to the storefront, so a full CSP is not something you can set yourself there. Check Shopify's documentation for what is supported.
Do not use a <meta http-equiv> tag for this. Meta tags do not support report-only mode or frame-ancestors. Use a real header.
Step 4: Read the violations
Reload your pages with DevTools open and look at the Console. In report-only mode, messages start with "[Report Only]". A typical one looks like this:
Refused to execute inline script because it violates the following Content Security Policy directive: "script-src 'self'". Either the 'unsafe-inline' keyword, a hash ('sha256-...'), or a nonce ('nonce-...') is required to enable inline execution.
For ongoing monitoring, add a report-uri directive pointing at a collector endpoint (several hosted services exist). The newer report-to mechanism is documented on MDN, but report-uri still has the widest browser support.
Fixing "blocks inline scripts" without unsafe-inline
This is the error that trips most people. An inline script is JavaScript written directly in your HTML, like <script>doThing()</script> or onclick="...". Because attackers inject inline scripts too, the browser blocks all of them unless you opt in.
The lazy fix is 'unsafe-inline'. It tells the browser to run every inline script, including injected ones, which removes most of what a CSP protects you from. That is why the term is in the name. Use these options instead, best first:
- Move it to a file. Put the code in
app.jsand load it with<script src="/app.js"></script>. Replaceonclick="..."attributes withaddEventListenercalls. - Use a hash for fixed scripts. The browser runs an inline script only if its fingerprint matches. Generate it on a Linux or Mac terminal (the text must match exactly, including spaces):
echo -n 'doThing()' | openssl dgst -sha256 -binary | openssl base64
Then add the output to your policy: script-src 'self' 'sha256-PASTE_RESULT_HERE'. Chrome's console error also prints the exact hash for you.
- Use a nonce for dynamic pages. A nonce is a random one-time value your server generates on every response. Add it to the header (
script-src 'nonce-R4nd0mValue') and to each trusted tag (<script nonce="R4nd0mValue">). It must be new on every page load, never a fixed value. - Last resort:
'unsafe-inline'. If you add a nonce or hash, modern browsers ignore'unsafe-inline'in the same directive, so some teams keep it only as a fallback for very old browsers. On its own, it gives little protection for scripts.
Styles are different. If your theme uses inline style="..." attributes, allowing style-src 'self' 'unsafe-inline' is a common, lower-risk compromise than doing the same for scripts. It is still not zero risk.
Common directives and their fixes
| Directive | What it controls | Typical error | Fix |
|---|---|---|---|
script-src | JavaScript | Inline script blocked | Move to a file, or add a hash or nonce |
script-src | Third-party scripts | Analytics or chat widget blocked | Add that vendor's exact script domain, per its docs |
style-src | CSS | Inline styles or style tags blocked | Move to a stylesheet, or allow 'unsafe-inline' for styles only |
img-src | Images | Images from a CDN blocked | Add the CDN domain, or keep https: |
font-src | Web fonts | Google Fonts blocked | Add https://fonts.gstatic.com, and https://fonts.googleapis.com in style-src |
connect-src | AJAX, fetch, WebSockets | API calls fail | Add the API domain, for example https://api.stripe.com |
frame-src | Iframes you embed | YouTube or payment frame blocked | Add the embed domain, such as https://www.youtube.com |
frame-ancestors | Who may embed your site | Your own widget page fails to embed | Use 'self', or list the trusted parent domains |
form-action | Where forms may submit | Form to another domain blocked | Add that domain, or keep 'self' |
Always copy third-party domains from the vendor's own documentation. For example, Stripe publishes its required CSP entries in its security guide. Lists change over time, so do not rely on old blog posts, including this one.
Step 5: Switch from report-only to enforce
- Run report-only for at least a week or two with real traffic, so rare pages and form flows get tested.
- Fix or consciously accept each violation. Do not add domains you do not recognize.
- Test login, checkout, contact forms and any admin screens.
- Rename the header from
Content-Security-Policy-Report-OnlytoContent-Security-Policy. - Keep a report-only copy running alongside it. That way you can test the next change safely.
- Know your rollback: removing the one header line instantly undoes the policy.
If you want a second opinion on a draft, Google's CSP Evaluator flags weak patterns.
Mistakes that weaken a policy
- Adding
*or barehttps:toscript-src. That allows scripts from almost anywhere. - Allowing
data:inscript-src. - Typos. Browsers ignore directives they do not understand (Chrome logs a console warning such as 'Unrecognized Content-Security-Policy directive'), so a misspelled directive protects nothing. Check the header in DevTools under Network, then Response Headers, and watch the Console.
- Treating the CSP as the finish line. It works best next to other security headers and fixed vulnerabilities.
Where a human audit fits
A CSP limits the impact of a script injection. It does not tell you whether your site has one. Automated tools can check that the header exists, but a person can judge whether your policy is weak, and can test whether the underlying bug is there. We explain the difference in manual vs automated penetration testing. To start for free, try the free website security check.
Key takeaways
- Always deploy a new CSP as
Content-Security-Policy-Report-Onlyfirst, so nothing breaks while you learn. - Fix inline script errors by moving code to files, or by using hashes or nonces, not
'unsafe-inline'. - Copy third-party domains from each vendor's docs, and review the list for anything you do not recognize.
- Enforce only after one to two weeks of clean reports, and keep a one-line rollback ready.
- A CSP is a safety net. It does not replace fixing the underlying vulnerabilities.
Next step
If you would like a person to check your CSP and look for the injection bugs it is meant to backstop, the next step is a manual audit. Circuit is $49 one-time: a security engineer reviews your site by hand and sends a written report with severity, evidence and exact fixes. It is a focused small-site audit, not a multi-week enterprise pentest or a compliance certificate.
A real human security engineer audits your whole site by hand and sends a full report: every issue, its severity, and the exact fix. From $49, with a 14-day money-back guarantee.
See pricing