Content Security Policy: Set It Up Safely

By Bug Circuit Security Team
Content Security Policy: Set It Up Safely

Is your own website affected? Check it free in 2 minutes.

Passive outside check only. Nothing is attacked, no signup.

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.

  1. Open your site in Chrome or Firefox and press F12 to open DevTools.
  2. Go to the Network tab and reload the page.
  3. Note every domain that is not your own: analytics, chat widgets, fonts, payment forms, video embeds, maps.
  4. 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 embedded data: 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 server block, add add_header Content-Security-Policy-Report-Only "default-src 'self'; script-src 'self'; object-src 'none'" always; (use your full policy), then run nginx -t and reload.
  • Apache: with mod_headers enabled, add Header 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:

  1. Move it to a file. Put the code in app.js and load it with <script src="/app.js"></script>. Replace onclick="..." attributes with addEventListener calls.
  2. 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.

  1. 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.
  2. 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

DirectiveWhat it controlsTypical errorFix
script-srcJavaScriptInline script blockedMove to a file, or add a hash or nonce
script-srcThird-party scriptsAnalytics or chat widget blockedAdd that vendor's exact script domain, per its docs
style-srcCSSInline styles or style tags blockedMove to a stylesheet, or allow 'unsafe-inline' for styles only
img-srcImagesImages from a CDN blockedAdd the CDN domain, or keep https:
font-srcWeb fontsGoogle Fonts blockedAdd https://fonts.gstatic.com, and https://fonts.googleapis.com in style-src
connect-srcAJAX, fetch, WebSocketsAPI calls failAdd the API domain, for example https://api.stripe.com
frame-srcIframes you embedYouTube or payment frame blockedAdd the embed domain, such as https://www.youtube.com
frame-ancestorsWho may embed your siteYour own widget page fails to embedUse 'self', or list the trusted parent domains
form-actionWhere forms may submitForm to another domain blockedAdd 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

  1. Run report-only for at least a week or two with real traffic, so rare pages and form flows get tested.
  2. Fix or consciously accept each violation. Do not add domains you do not recognize.
  3. Test login, checkout, contact forms and any admin screens.
  4. Rename the header from Content-Security-Policy-Report-Only to Content-Security-Policy.
  5. Keep a report-only copy running alongside it. That way you can test the next change safely.
  6. 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 bare https: to script-src. That allows scripts from almost anywhere.
  • Allowing data: in script-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-Only first, 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.

Want certainty, not guesswork?

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

Frequently asked questions

How do I implement a Content Security Policy?
Send a `Content-Security-Policy-Report-Only` header first with a strict starter policy, then read the browser console for violations. Fix each one by moving inline code to files, using hashes or nonces, or allowing trusted vendor domains. After a week or two of clean reports, rename the header to `Content-Security-Policy` to enforce it.
What is Content Security Policy report-only mode?
Report-only mode applies your policy in test mode. The browser logs or reports every violation but blocks nothing, so your site keeps working. It is the safest way to find what a policy would break before you enforce it.
Is unsafe-inline safe in a Content Security Policy?
Not for scripts. 'unsafe-inline' lets the browser run any inline script, including one an attacker injects, which removes most of the protection a CSP offers. Use external files, hashes or nonces instead. For styles it is a more common and lower-risk compromise.
Why does my Content Security Policy block inline scripts?
By default a CSP that sets script-src without 'unsafe-inline', a hash or a nonce refuses to run any JavaScript written directly in your HTML, including onclick attributes. Move the code to a .js file and replace onclick attributes with addEventListener calls. A hash or nonce can allow a specific inline <script> block you trust, but it does not allow inline event-handler attributes such as onclick (that would need 'unsafe-hashes', which is not recommended).
Will adding a CSP break my WordPress site?
It can, because themes and plugins often use inline scripts and styles. That is why you start in report-only mode and test your key pages before enforcing. Expect to allow some vendor domains and possibly inline styles, and re-test after plugin updates.

Keep reading

See what attackers see, for free

Run the free passive check on your website. No login, no impact on your site, results in seconds.

Ready for the full manual audit? See pricing

Published by Bug Circuit. Written with AI assistance and reviewed for accuracy before publishing.