Refused to Load the Script — Fixing reCAPTCHA and Turnstile Under a Strict Content-Security-Policy

"Refused to Load the Script" — Fixing reCAPTCHA and Turnstile Under a Strict Content-Security-Policy

Your security team finally ships a strict Content-Security-Policy, the audit passes — and the signup form dies. The widget area is blank, the console is full of violation reports, and nothing reaches your server. Here are the exact directives reCAPTCHA and Cloudflare Turnstile need, the nonce option for stricter policies, and the debugging workflow that keeps hardening from becoming an outage.

rCAPTCHA Team
rCAPTCHA Team
June 10, 2026 · 10 min read

A Content-Security-Policy is an allowlist: anything you do not explicitly permit, the browser refuses to load. That is exactly what makes CSP one of the strongest defenses against cross-site scripting — and exactly why it breaks third-party widgets first. reCAPTCHA is not one resource but a chain of them: a script from one Google domain, supporting code from another, and an iframe that hosts the actual challenge. Miss any link in that chain and the widget never appears, the g-recaptcha-response field stays empty, and every form submission fails server-side verification. The browser tells you precisely what it blocked — "Refused to load the script ... because it violates the following Content Security Policy directive" — but only in the console, where users never look.

The Exact Directives reCAPTCHA Needs

Try rCAPTCHA

Experience the technology discussed in this article.

Learn More →

Google's official reCAPTCHA FAQ documents the policy entries the widget requires. For a domain-allowlist CSP, the recommended form scopes the permissions to the reCAPTCHA paths rather than whole hosts:

script-src https://www.google.com/recaptcha/
           https://www.gstatic.com/recaptcha/release/
frame-src  https://www.google.com/recaptcha/
           https://recaptcha.google.com/recaptcha/

Two details in there cause most of the retry-and-fail loops we see in forum threads. First, both script-src entries are needed: api.js is served from www.google.com/recaptcha/, but it then loads the actual widget code from www.gstatic.com/recaptcha/release/. Allowing only the first host produces a widget that half-loads and then dies. Second, frame-src is not optional. The checkbox, the image challenges, and even the invisible variant's challenge fallback all live inside an iframe; if frame-src (or its legacy parent child-src, or a restrictive default-src with no frame-src override) blocks Google's frame, users see either an empty box or a checkbox that spins forever.

If your site serves users through the recaptcha.net endpoint — common for audiences where google.com is unreachable, as we covered in our recaptcha.net guide for China — mirror every directive for www.recaptcha.net as well. A CSP that only names the Google hosts will block the alternate endpoint just as thoroughly as the Great Firewall blocks the original.

Why you should not just allowlist google.com

The tempting shortcut — script-src https://www.google.com with no path — works, but it quietly weakens the policy you just deployed. Large domains host endpoints that can return script in attacker-influenced shapes (the classic example being JSONP-style endpoints), which is why CSP auditing tools such as Google's own CSP Evaluator flag broad allowlists of big multi-product domains. Scoping to /recaptcha/ paths keeps the widget working while leaving the rest of the domain outside your trust boundary. The official google/recaptcha repository ships a CSP example demonstrating a working policy — worth comparing against yours before you start loosening directives in frustration.

The nonce option for strict policies

Teams running a strict, nonce-based CSP (where script-src trusts only tagged scripts rather than domains) do not have to fall back to domain allowlists. Per Google's FAQ, the reCAPTCHA script tag accepts a nonce:

<script src="https://www.google.com/recaptcha/api.js"
        nonce="YOUR_RANDOM_NONCE" async defer></script>

The nonce must be generated per response and echoed in the Content-Security-Policy header — a static nonce is no policy at all. You still need the frame-src entries, because nonces apply to scripts, not to where iframes may point.

Cloudflare Turnstile: One Host, Same Two Directives

Turnstile keeps the list shorter. Cloudflare's official CSP reference names a single host for both the script and the challenge frame:

script-src https://challenges.cloudflare.com
frame-src  https://challenges.cloudflare.com

The failure modes rhyme with reCAPTCHA's: allow the script but not the frame and you get Turnstile's error-code family instead of a token — if you are staring at codes like 300030 or 600010, our Turnstile error-code guide walks the full decode table. CSP belongs near the top of that investigation whenever the codes appear only on your hardened production domain and never on staging.

A Debugging Workflow That Doesn't Involve Guessing

  1. Read the violation, not the symptom. Open DevTools on the broken page. Every CSP block produces a console line naming the blocked URL and the directive that blocked it. That line is the entire diagnosis: add that URL's origin (and path prefix, where supported) to that directive.
  2. Fix directives one violation at a time. The widget loads resources in sequence, so fixing script-src may reveal a new frame-src violation. Repeat until the console is clean and the widget renders.
  3. Use Report-Only mode for rollout. Deploying Content-Security-Policy-Report-Only first lets the browser report what it would block without breaking production forms — the difference between an inbox of reports and an inbox of angry users.
  4. Find all your policy sources. Multiple CSP headers combine restrictively, and policies arrive from more places than your app code: web-server config, CDN page rules, hosting "security hardening" panels, and WordPress security plugins (CAPTCHA-plugin vendors publish their own CSP guides for exactly this reason). A perfect header in your app is still defeated by a stricter one injected upstream.
  5. Re-test the full submit path. A rendering widget is not the finish line — submit the form and confirm the token verifies. If the widget renders but tokens fail, you have left CSP territory; start with our script-loading and browser-error guide.

The Real Lesson: Third-Party Trust Is a Line Item

Every CSP entry above is a statement: this outside party may run code on my pages and frame content into them. Writing the directives forces a question that auto-loading scripts let teams skip — which third parties does this site actually trust, and on which pages? That is a healthy audit to run annually regardless of CAPTCHAs. It is also fair to note that verification layers differ in how much CSP surface they demand: a system like rCAPTCHA's behavioral verification keeps the third-party allowlist small, though any externally loaded verification — ours included — belongs in your policy explicitly rather than under a wildcard.

People Also Ask: CSP and CAPTCHA FAQ

Which CSP directives does reCAPTCHA require?

script-src entries for https://www.google.com/recaptcha/ and https://www.gstatic.com/recaptcha/release/, plus frame-src entries for https://www.google.com/recaptcha/ and https://recaptcha.google.com/recaptcha/. Sites using the recaptcha.net endpoint must mirror the entries for that host.

Why does reCAPTCHA work on staging but not production?

Usually because the CSP differs: production fronts often add security headers at the CDN, load balancer, or hosting-panel level that staging never sees. Compare the actual response headers from both environments rather than the application config.

Can I use a nonce instead of allowlisting Google's domains?

Yes — the reCAPTCHA script tag supports a per-response nonce attribute for strict CSPs. You still need the frame-src allowlist entries, since nonces do not apply to iframe destinations.

What CSP does Cloudflare Turnstile need?

Add https://challenges.cloudflare.com to both script-src and frame-src. That single host serves both the api.js script and the challenge iframe.

Conclusion

A CAPTCHA that vanishes behind a new security header is not a widget bug — it is your allowlist doing its job on a dependency nobody wrote down. The fix is five minutes once you read the violation message: scope script-src and frame-src to the documented reCAPTCHA or Turnstile paths, prefer path-scoped entries or nonces over whole-domain trust, roll out in Report-Only mode, and keep the directives in version control next to the integration they protect. Hardening and working forms are not in conflict — they just have to be deployed by the same checklist.

Sources & Further Reading

rCAPTCHA Blog
rCAPTCHA Blog

Insights on web security and bot detection

More from this blog →

Responses

No responses yet. Be the first to share your thoughts!