[Self-hosted] Cloudflare Turnstile captcha is blocked by a CSP that only allows hCaptcha

(#652) Bug Fixed self-hosting

Observed behaviour

  • The bootstrap JSON delivered by the app correctly contains:
      "captcha":{
        "provider":"turnstile",
        "turnstile_site_key":"<redacted>",
        "hcaptcha_site_key":null
      }
  • No widget appears on the registration form.
  • The browser console shows a CSP violation along the lines of:and a matching frame-src violation for the iframe.
Expected behaviour A Cloudflare Turnstile widget renders on the registration form, just like hCaptcha does when hcaptcha is selected as the provider.

Reproduction steps

  1. Deploy the self-hosting stack.
  2. In the admin console (/admin), under Bot protection, select Cloudflare Turnstile as the provider and enter a valid site key + secret key.
  3. Set the environment variables:
  4. Open the public site in a fresh browser tab and start the registration flow.

Details

In Short

When FLUXER_CAPTCHA_PROVIDER=turnstile is configured on a self-hosted instance, the bootstrap payload correctly advertises the Turnstile site key, but the Content-Security-Policy served by the app shell only whitelists hCaptcha. As a result, the browser refuses to load https://challenges.cloudflare.com/turnstile/v0/api.js and no captcha widget is rendered on the registration page. In effect, Turnstile is selectable in the admin UI but non-functional out of the box.

Environment

  • Deployment: self-hosted via Docker Compose (Coolify, but the issue is not Coolify-specific — the CSP is set by the app, not the proxy)
  • Image tag: ghcr.io/fluxerapp/fluxer-app-proxy-self-hosted:v1
  • Caddy (caddy:2.10-alpine) used as the documented reverse proxy
  • Browser: Firefox / Chromium (current versions)

Root cause (best guess)

The CSP emitted by app-proxy is hardcoded with hCaptcha origins:
  • script-src includes https://hcaptcha.com https://*.hcaptcha.com
  • frame-src includes https://hcaptcha.com https://*.hcaptcha.com
but does not include the Turnstile origins:
  • https://challenges.cloudflare.com (script + iframe + connect)
  • https://static.cloudflareinsights.com (sometimes used by Turnstile telemetry)
Because the captcha provider is selectable at runtime, the CSP should reflect the active provider — either by emitting a different allowlist depending on FLUXER_CAPTCHA_PROVIDER, or by including both providers' origins unconditionally.

Suggested fix

When FLUXER_CAPTCHA_PROVIDER=turnstile, extend the CSP served by app-proxy (and any other component that emits CSP for the web client) to include at least:
  • script-src → https://challenges.cloudflare.com
  • frame-src → https://challenges.cloudflare.com
  • connect-src → https://challenges.cloudflare.com
Alternatively, allow operators to configure additional CSP sources via env vars (e.g. FLUXER_CSP_EXTRA_SCRIPT_SRC, FLUXER_CSP_EXTRA_FRAME_SRC) so the same kind of issue can be solved for other future providers without code changes.

Workaround for other self-hosters reading this

Switch the provider to hCaptcha in the admin console (FLUXER_CAPTCHA_PROVIDER=hcaptcha). It is already whitelisted in the default CSP and works without further configuration.

Additional notes

  • A minor unrelated nit in deploy/self-hosting/docker-compose.yml: the captcha defaults appear in some forks/edits as :true / :turnstile instead of :-false / :-none. The current upstream is correct, but it might be worth a brief comment near those lines explaining that the operator should set FLUXER_CAPTCHA_PROVIDER via env, since people copy-paste these blocks and easily drop the -.
  • Happy to provide a redacted HAR / full CSP header if useful.

Logs or screenshots

Browser Console: "RestTransport.ts:501 Loading the script 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onloadTurnstileCallback&render=explicit%27 violates the following Content Security Policy directive: "script-src 'self' 'nonce-93b7c18a12fdfe08b50ceb0c7cbf3c24' 'wasm-unsafe-eval' blob: https://*.fluxer.app/ https://hcaptcha.com/ https://*.hcaptcha.com/ https://example.domain/". Note that 'script-src-elem' was not explicitly set, so 'script-src' is used as a fallback. The action has been blocked"

4 comments

Sign in with Fluxer to comment and vote.
Comment by @MrRubberDucky
RexSystem 1 vote edited originally by @MrRubberDucky on GitHub
There's already a better workaround available in #586 for this by Buco, you don't have to use hCaptcha. But yes, customizable or modifiable CSP headers would be a great feature to have that would solve most of the current CSP-related problems.
Comment by @Float-as
RexSystem 1 vote originally by @Float-as on GitHub
Same issue for forgot password if turnstile is configured. Disabled it and I can complete forgot password flow successfully. EDIT: Hmmm same with hCaptcha 👀
Comment by @b-hayes
RexSystem 1 vote originally by @b-hayes on GitHub
hit this too on a self-hosted setup. worked around it at the reverse proxy for now - since hcaptcha.com is already in the CSP directives turnstile needs, i just append the turnstile origin next to it. caddy example:
reverse_proxy app-proxy:8080 {
    header_down Content-Security-Policy "https://hcaptcha.com " "https://hcaptcha.com https://challenges.cloudflare.com "
}
that covers script/style/connect/frame in one go and the widget renders. obviously just a stopgap til the CSP knows about the turnstile provider, but figured it'd help anyone else stuck on the empty captcha box.