If a Content-Security-Policy (CSP) broke your WordPress site, it's blocking something the page needs: a script, a stylesheet, a font, an iframe or a tracking request such as GA4. Stop enforcing the policy so the site works again, switch the header to Content-Security-Policy-Report-Only, read the browser console errors and add the sources each tool needs. Don't enforce it again until the console stays clean on every template.
I wrote the HTTP Security Header plugin for WordPress, which now has 1,000+ active installs. Of all the headers it can set, CSP is the only one I'd call risky. HSTS, Referrer-Policy and the rest rarely change how a page renders. CSP decides whether each script and style is allowed to run at all. My earlier guide covers adding security headers to WordPress. This one is for after something has gone wrong.
What does a broken Content Security Policy look like on WordPress?
A broken CSP on WordPress usually shows up as a blank or unstyled page, an empty HubSpot form box, a missing Stripe card field, no GA4 data or an Elementor preview that won't load. Each symptom points to a specific directive. Here's where I'd look first:
| Symptom | Directive usually at fault | Typical cause |
|---|---|---|
| Blank or half-rendered page, menus and sliders dead | script-src or default-src | Inline scripts or theme/plugin JavaScript blocked |
| Page unstyled, or fonts fall back to Arial/Times | style-src, font-src | Inline styles, Google Fonts or a CDN stylesheet blocked |
| GA4 shows no data, GTM tags don't fire | script-src, connect-src, img-src | Google domains missing, inline GTM snippet blocked |
| HubSpot form area is an empty box | script-src, style-src, connect-src, frame-src | HubSpot's script and form domains missing |
| Stripe card field missing or 3D Secure fails | frame-src, script-src, connect-src | Stripe.js frames or hooks.stripe.com blocked |
| Elementor editor says the preview could not be loaded | frame-ancestors, script-src | frame-ancestors 'none' or blocked inline admin scripts |
| YouTube or Maps embeds blank | frame-src | Embed domain not allowed |
One rule explains a lot of these. If a specific directive such as script-src isn't set, the browser falls back to default-src, as MDN's CSP reference explains. So a policy that only says default-src 'self' blocks every external script, font and iframe on the site.
How do I read CSP errors in the browser console?
CSP errors show up in the browser console as red "Refused to load" or "Refused to execute inline script" messages, and each one names the blocked URL and the directive that blocked it. Open the broken page in Chrome or Edge, press F12 and go to the Console tab. (The Issues tab groups the same errors.) Every violation tells you what was blocked, which directive blocked it and the policy currently in force. A typical one reads:
Refused to load the script 'https://js.hsforms.net/forms/embed/v2.js'
because it violates the following Content Security Policy directive:
"script-src 'self' https://www.googletagmanager.com".
So you add https://js.hsforms.net to script-src. Easy. The other message you'll see a lot on WordPress is about inline code:
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.
This one isn't about a domain. WordPress core, page builders and most plugins print small <script> and <style> blocks straight into the HTML. A request to let WordPress core run under a CSP without 'unsafe-inline' has been open on WordPress Trac ticket #39941 for years, so expect this error on almost every WordPress site.
Check the console logged out, logged in and on every template type: home, blog post, a page with a form, cart, checkout and My Account. They all load different scripts.
How do I roll back a broken CSP fast?
To roll back a broken CSP, disable the header where it's set (a plugin, .htaccess, Nginx, Cloudflare or your host's panel), then purge every cache and confirm no Content-Security-Policy header is being sent. If customers are hitting the broken page right now, get the site working first and debug later.
- 1
Find where the header comes from
Check the response headers in DevTools (Network tab, click the document) or run the URL through my free Security Headers Checker. The header could come from a plugin, .htaccess, Nginx config, Cloudflare or your host's panel.
- 2
Disable it at the source
Clear the CSP field in the plugin, comment out the Header line in .htaccess, or turn off the Cloudflare transform rule. If wp-admin itself is broken, rename the plugin's folder over SFTP.
- 3
Purge every cache
Clear your page cache plugin, server cache and CDN. A cached page can keep serving the old header after you've removed it.
- 4
Confirm it's gone
Reload in a private window and recheck the headers. Only move on once no Content-Security-Policy header is being sent.
My free Security Headers Checker shows every header a URL sends, including duplicate CSPs.
Two CSP headers means both are enforced
If a security plugin, your host and Cloudflare each send a CSP, the browser applies every one of them, so a looser second policy can't undo a strict first one. Remove the duplicates and keep one source of truth.
That rule is in MDN's CSP header reference. A duplicate header is often the reason someone adds a domain, sees nothing change and decides CSP just doesn't work.
Why switch to Content-Security-Policy-Report-Only first?
Content-Security-Policy-Report-Only lets you test a policy on a live WordPress site without breaking anything. It uses the same syntax as the enforced header, but the browser only logs violations and blocks nothing. Visitors get a working site while you watch the console, where these violations appear prefixed with [Report Only]. It has to be sent as a real HTTP header, though. Report-only mode doesn't work in a <meta> tag.
If you want violations from real visitors, not just your own browser, add a reporting endpoint. MDN covers the older report-uri directive and its replacement, report-to. On a small business site, clicking through every template yourself with the console open catches most problems. I'd still leave report-only running for a normal week of traffic, because some forms, payments and campaign tags only fire now and then.
Which sources do GA4, GTM, HubSpot, Stripe and Elementor need?
GA4 and Google Tag Manager need Google's tag and analytics domains, Stripe needs js.stripe.com, api.stripe.com and hooks.stripe.com, HubSpot needs its hsforms and hs-scripts domains, and Elementor needs 'unsafe-inline' plus frame-ancestors 'self'. Build the policy one tool at a time, and start from the vendor's own documentation wherever there is some.
| Tool | What to allow | Source |
|---|---|---|
| GA4 (no Ads features) | script-src https://*.googletagmanager.com; img-src https://*.google-analytics.com https://*.googletagmanager.com; connect-src https://*.google-analytics.com https://*.analytics.google.com https://*.googletagmanager.com | Google Tag Platform CSP guide |
| Google Tag Manager container | A nonce on the inline snippet (recommended) or 'unsafe-inline', plus the domains of every tag inside the container | Google Tag Platform CSP guide |
| Stripe.js (WooCommerce Stripe, Elements) | script-src https://js.stripe.com https://*.js.stripe.com; frame-src https://js.stripe.com https://*.js.stripe.com https://hooks.stripe.com; connect-src https://api.stripe.com | Stripe integration security guide |
| HubSpot forms and tracking | Typically js.hsforms.net, forms.hsforms.com, js.hs-scripts.com, js.hs-analytics.net, js.hs-banner.com, forms.hubspot.com, plus style-src 'unsafe-inline'. Confirm against your own console reports | HubSpot Community CSP thread |
| Google Fonts | style-src https://fonts.googleapis.com; font-src https://fonts.gstatic.com | Or self-host the fonts and drop both |
| Elementor and WordPress core | 'unsafe-inline' in style-src (and usually script-src), frame-ancestors 'self' | Confirm against your own console reports and the Elementor editor |
A few of these catch people out.
Google TLDs. Google notes that CSP can't wildcard the top-level domain, so each Google TLD has to be listed on its own. If you use the Google Ads features and get visitors from the UK or India, that can mean listing google.co.uk or google.co.in next to google.com. Every GTM tag also brings its own domains, so recheck the console whenever the container changes.
Stripe 3D Secure. Stripe's guide says hooks.stripe.com must be in frame-src for cards that need 3D Secure. Leave it out and your test cards pass while real customers fail their bank check. If WooCommerce checkout still fails after the CSP is sorted, work through my WooCommerce Stripe payments fix guide.
HubSpot. HubSpot doesn't publish one tidy CSP list, and people in that community thread are still piecing their own together. The thread also points out that HubSpot forms use inline style attributes, which pushes you towards 'unsafe-inline' in style-src whether you like it or not.
Here's a starting policy to send in report-only mode for a WordPress page-builder site with GA4, Stripe and YouTube. It's a draft for your console to correct, not a finished policy:
Content-Security-Policy-Report-Only:
default-src 'self';
script-src 'self' 'unsafe-inline' https://*.googletagmanager.com https://js.stripe.com https://*.js.stripe.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' data: https://fonts.gstatic.com;
img-src 'self' data: https:;
connect-src 'self' https://*.googletagmanager.com https://*.google-analytics.com https://*.analytics.google.com https://api.stripe.com;
frame-src https://js.stripe.com https://*.js.stripe.com https://hooks.stripe.com https://www.youtube-nocookie.com;
frame-ancestors 'self';
object-src 'none';
base-uri 'self'
Send it as a single line. The line breaks are only there so you can read it.
Should I use 'unsafe-inline' or nonces on WordPress?
On most Elementor, Bricks or WooCommerce sites, use 'unsafe-inline' in style-src and accept it in script-src as a known compromise. Nonces are stronger, but they must be unique per response, and that clashes with full-page caching.
A nonce is a random value added to the CSP and to each inline <script> tag. Google's guide is clear that it has to be generated fresh for every response. WordPress 5.7 added functions and filters for script attributes, such as wp_get_inline_script_tag() and the wp_inline_script_attributes filter, so scripts that go through the proper APIs can carry a nonce. Two problems are left:
- Any plugin that echoes a raw
<script>tag skips those APIs and gets blocked. - Full-page caching serves the same HTML, with the same nonce, to every visitor, which defeats the point. Getting nonces and aggressive page caching to work together takes careful engineering.
So on a typical Elementor, Bricks or WooCommerce site, I'd ship 'unsafe-inline' in style-src and not lose sleep over it. In script-src, live with it for now while you tighten everything else. A policy that limits which domains can load scripts and sets object-src 'none', base-uri 'self' and frame-ancestors 'self' still stops real attacks. Nonces earn their keep on custom themes or headless builds where you control every script tag.
You also need to decide whether wp-admin gets the same policy as the front end. The block editor, Elementor's editor and lots of admin plugins rely on inline code. Plenty of teams apply the strict policy to public pages only. I think that's a sensible call.
How do I switch back to an enforced CSP safely?
Switch back to an enforced CSP only when the report-only console stays quiet across every template. Then rename the header to Content-Security-Policy and test the things that make you money. Submit a real form and check it lands in HubSpot or your inbox, run a Stripe test payment with a 3D Secure card, look at GA4 Realtime and open the Elementor editor. Run the page through the Security Headers Checker to confirm exactly one CSP is being sent.
You can also send both headers at once: enforce the policy you know works, and test a stricter version in report-only mode alongside it. That's how I'd tighten a policy, say by removing 'unsafe-inline' from script-src, without risking another outage. Recheck whenever you add a plugin, an embed or a GTM tag.
When is it worth handing this to a developer?
A brochure site with GA4 and one contact form is usually an afternoon's work with this guide. A WooCommerce checkout, a busy GTM container or a move to nonces is different: mistakes cost orders and leads, and the fix spans caching, theme code and server config. I handle this as part of my technical SEO and Core Web Vitals service.
Next step: run your site through the free Security Headers Checker to see every header you're sending. If the CSP is still breaking something, or you'd rather have a policy built and tested for you, get in touch with the URL and a screenshot of the console errors.



