Content Security Policy broke WordPress? How to fix it

Content Security Policy broke your WordPress site? Roll back safely, switch to Report-Only and fix blocked Stripe, HubSpot and GA4 sources.

Mohit Goyal10 min read
Content Security Policy broke WordPress? How to fix it

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:

SymptomDirective usually at faultTypical cause
Blank or half-rendered page, menus and sliders deadscript-src or default-srcInline scripts or theme/plugin JavaScript blocked
Page unstyled, or fonts fall back to Arial/Timesstyle-src, font-srcInline styles, Google Fonts or a CDN stylesheet blocked
GA4 shows no data, GTM tags don't firescript-src, connect-src, img-srcGoogle domains missing, inline GTM snippet blocked
HubSpot form area is an empty boxscript-src, style-src, connect-src, frame-srcHubSpot's script and form domains missing
Stripe card field missing or 3D Secure failsframe-src, script-src, connect-srcStripe.js frames or hooks.stripe.com blocked
Elementor editor says the preview could not be loadedframe-ancestors, script-srcframe-ancestors 'none' or blocked inline admin scripts
YouTube or Maps embeds blankframe-srcEmbed 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:

text
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:

text
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. 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. 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. 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. 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.

ToolWhat to allowSource
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.comGoogle Tag Platform CSP guide
Google Tag Manager containerA nonce on the inline snippet (recommended) or 'unsafe-inline', plus the domains of every tag inside the containerGoogle 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.comStripe integration security guide
HubSpot forms and trackingTypically 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 reportsHubSpot Community CSP thread
Google Fontsstyle-src https://fonts.googleapis.com; font-src https://fonts.gstatic.comOr 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:

text
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.

Frequently asked

Why did my WordPress site go blank after I added a Content-Security-Policy header?+

A blank WordPress page after adding a CSP almost always means script-src or default-src is blocking scripts the page needs, such as inline scripts, jQuery or the theme's JavaScript. Open the browser console: the red 'Refused to load' or 'Refused to execute inline script' messages name exactly what was blocked.

How do I turn off a Content Security Policy on WordPress if I can't log in to wp-admin?+

Connect to the server by SFTP or your host's file manager. If a plugin sets the CSP header, rename that plugin's folder in wp-content/plugins. If the header is in .htaccess, comment out the Header line. Then purge the page cache and CDN so the old header stops being served.

What is Content-Security-Policy-Report-Only and when should I use it?+

Content-Security-Policy-Report-Only uses the same syntax as Content-Security-Policy, but the browser only reports violations and blocks nothing. Use it to test a CSP on a live WordPress site without breaking pages. It has to be sent as an HTTP header, because it can't be set through a meta tag.

Does Elementor work with a Content-Security-Policy?+

Yes, as long as the policy allows the inline styles and scripts that Elementor and WordPress core output, which usually means 'unsafe-inline' unless nonces are set up. Keep frame-ancestors at 'self' rather than 'none', or the Elementor editor preview can fail to load in its iframe.

Which CSP domains does Stripe need on a WooCommerce checkout?+

Stripe's documentation lists js.stripe.com and *.js.stripe.com for script-src and frame-src, api.stripe.com for connect-src, and hooks.stripe.com in frame-src for 3D Secure and redirect-based payment methods. If any of these is missing, WooCommerce card fields or bank authentication can fail without an obvious error.

Is a Content Security Policy with 'unsafe-inline' still worth having on WordPress?+

Yes, though it's weaker. Domain allowlists plus object-src 'none', base-uri 'self' and frame-ancestors 'self' still block real attacks such as clickjacking and scripts loaded from unknown hosts. Treat 'unsafe-inline' as a step toward a nonce-based policy.

Mohit Goyal

Written by Mohit Goyal

Senior Web Developer in Gurugram, India. 100+ fast, SEO-friendly sites shipped in 4+ years with Next.js, React and WordPress, including the procol.ai rebuild. Send me your next project.

Keep reading

How to add HTTP security headers to WordPress

Security headers protect a WordPress site from clickjacking, content sniffing and XSS. What each header does and how to add them without editing server files.

5 min read

Elementor update broke my site: how to fix it safely

Elementor update broke your site? Roll back safely, regenerate CSS, find the conflict in Safe Mode, then stop it happening again.

11 min read

Let's build

Have a site that needs to be fast, rank and convert?

Open to Work. Usually replies within a day.