CSP for Stripe.js: strict vs practical payment policies

Table of Contents

If you’ve ever tried to lock down Content Security Policy around Stripe.js, you already know the annoying truth: payments are where clean CSP theory collides with third-party reality.

Stripe.js is not just a single script include. It brings frames, network calls, fraud tooling, and region-specific behavior. If your CSP is too strict, checkout breaks. If it’s too loose, you’ve basically given up and written https: everywhere.

I’ve seen teams do both.

This guide compares the main CSP approaches for Stripe.js payment integration, with pros and cons, and shows what I’d actually ship for a developer-facing production app.

The core problem with Stripe.js and CSP

A modern Stripe integration usually needs some combination of:

  • js.stripe.com for the Stripe.js script
  • checkout.stripe.com and/or hooks.stripe.com for frames and supporting flows
  • api.stripe.com for requests
  • additional Stripe-controlled subdomains depending on payment methods, fraud detection, and hosted flows

That means a basic CSP like this:

Content-Security-Policy: default-src 'self'; script-src 'self'; frame-src 'self'; connect-src 'self';

will fail instantly.

The challenge is deciding how much flexibility to allow without turning your policy into mush.

A useful baseline: what a real strict-ish CSP looks like

Here’s a real CSP header from headertest.com:

content-security-policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-NjBiMzhiZWUtZTA4NC00MGM3LThhZjUtZTRlZWY2NTZlOGM4' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
style-src 'self' 'unsafe-inline' https://www.googletagmanager.com https://*.cookiebot.com https://consent.cookiebot.com;
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.headertest.com https://tallycdn.com https://or.headertest.com wss://or.headertest.com https://*.google-analytics.com https://*.googletagmanager.com https://*.cookiebot.com;
frame-src 'self' https://consentcdn.cookiebot.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'

This is a decent example of a site that’s trying to stay disciplined:

  • nonce-based scripts
  • strict-dynamic
  • no object-src
  • locked frame-ancestors
  • tight form-action and base-uri

But for Stripe.js, this policy is incomplete. It has no Stripe sources in script-src, connect-src, or frame-src, so Stripe Elements or Checkout would break.

That’s the pattern I see most often: a site gets serious about CSP, then payments become the first exception-heavy area.

Option 1: Broad allowlisting for Stripe domains

This is the most common approach. You explicitly allow Stripe in the directives it needs.

Example:

Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}' https://js.stripe.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.stripe.com https://r.stripe.com;
frame-src 'self' https://js.stripe.com https://hooks.stripe.com https://checkout.stripe.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self' https://checkout.stripe.com;
object-src 'none';

Pros

  • Easy to understand
  • Works well for Stripe Elements and many Checkout flows
  • Fits how most teams already manage third-party CSP sources
  • Low operational complexity

Cons

  • You’ll probably need to revisit it as Stripe adds or changes endpoints
  • Developers tend to overcompensate and add broad wildcards too early
  • If you mix this with lots of other vendors, your policy grows fast

My take

This is usually the right starting point. Not because it’s elegant, but because it’s maintainable.

I’d rather ship a deliberate allowlist than pretend I can build a perfect zero-trust policy around a payment provider that relies on embedded third-party resources.

Option 2: Wildcard-heavy Stripe policy

Some teams skip the endpoint churn and just allow broad Stripe host patterns:

Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}' https://js.stripe.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://*.stripe.com;
frame-src 'self' https://*.stripe.com;
form-action 'self' https://*.stripe.com;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';

Pros

  • Less brittle
  • Faster to get working across multiple Stripe products
  • Lower support burden when payment methods vary by region

Cons

  • Much wider trust boundary
  • Harder to justify in a security review
  • Easy to cargo-cult into “just allow everything from vendors”

My take

I only use this when the business genuinely needs multiple Stripe-hosted flows and the exact host list is unstable. Otherwise it’s lazy.

https://*.stripe.com is convenient, but convenience is not a security strategy.

Option 3: Strict nonce-based CSP plus Stripe allowlists

This is the setup I prefer for production apps that care about XSS, not just compliance screenshots.

You keep your own scripts under nonce control and only allow Stripe where necessary.

Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic' https://js.stripe.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.stripe.com https://r.stripe.com;
frame-src 'self' https://js.stripe.com https://hooks.stripe.com https://checkout.stripe.com;
form-action 'self' https://checkout.stripe.com;
base-uri 'self';
frame-ancestors 'none';
object-src 'none';
report-to csp-endpoint;
report-uri /csp-report;

And your page code:

<script nonce="{{ .CSPNonce }}" src="https://js.stripe.com/v3/"></script>
<script nonce="{{ .CSPNonce }}">
  const stripe = Stripe("pk_live_xxx");
  const elements = stripe.elements();

  const card = elements.create("card");
  card.mount("#card-element");
</script>

Pros

  • Strong protection against inline script injection
  • Good balance between app security and third-party compatibility
  • Easier to reason about than giant source lists in every directive
  • Plays nicely with modern frameworks that can emit nonces

Cons

  • More implementation work
  • Nonce plumbing can be annoying in SSR and edge-rendered apps
  • strict-dynamic can confuse teams who haven’t used it before

My take

If your app already supports CSP nonces, do this. It’s the least bad option.

Stripe still needs explicit source allowances, but your own script execution model stays tight. That matters more than people think, especially on checkout pages where attackers would love DOM XSS.

Option 4: Relaxed CSP just for the checkout route

This is the pragmatic route-specific model. Your main site stays strict, while /checkout gets a more permissive policy.

For example, your normal app policy might look like the headertest.com header style: nonce-based, strict-dynamic, narrow framing, no object embeds.

Then your checkout route gets something like:

Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}' https://js.stripe.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.stripe.com https://r.stripe.com;
frame-src 'self' https://js.stripe.com https://hooks.stripe.com https://checkout.stripe.com;
form-action 'self' https://checkout.stripe.com;
base-uri 'self';
frame-ancestors 'none';
object-src 'none';

Pros

  • Contains third-party sprawl to the payment surface
  • Easier to audit than one giant sitewide policy
  • Lets marketing pages and app dashboards remain much stricter

Cons

  • More policy management overhead
  • Easy for route mismatches to cause weird bugs
  • Requires discipline in deployment and testing

My take

I like this approach a lot. Checkout is already a special surface area. Treating it differently is reasonable.

Trying to force one CSP to fit your blog, admin dashboard, product UI, and payment flow usually ends badly.

What I would avoid

Blind unsafe-inline in script-src

Don’t do this unless you’ve given up.

script-src 'self' 'unsafe-inline' https://js.stripe.com;

Stripe is not the reason to allow arbitrary inline script execution. Use a nonce.

https: as a catch-all

This is the classic panic move:

script-src 'self' https:;
connect-src 'self' https:;
frame-src 'self' https:;

Yes, it works. It also guts most of the value of CSP.

Copy-pasting stale host lists

Stripe changes things. So do browsers. So do payment flows. If you copy a CSP snippet from a random blog post and never test it in report-only mode, you’re setting yourself up for flaky payments.

A practical recommendation

For most developer teams integrating Stripe.js, I’d rank the options like this:

  1. Strict nonce-based CSP plus focused Stripe allowlists
  2. Route-specific checkout CSP
  3. Broad explicit allowlisting
  4. Wildcard-heavy Stripe policy

That order changes if your app is simple. If you just need a hosted payment page quickly, broad allowlisting may be perfectly fine. Security is always contextual.

A decent starting policy for Stripe.js Elements looks like this:

Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{RANDOM}' 'strict-dynamic' https://js.stripe.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://api.stripe.com https://r.stripe.com;
frame-src 'self' https://js.stripe.com https://hooks.stripe.com https://checkout.stripe.com;
form-action 'self' https://checkout.stripe.com;
base-uri 'self';
frame-ancestors 'none';
object-src 'none';
report-uri /csp-report;

Then test the actual flows you support:

  • card payments
  • 3D Secure
  • wallet flows
  • embedded checkout vs hosted checkout
  • regional payment methods

That last part matters. A CSP that passes a happy-path Visa test card is not a real payment policy.

When to use ready-made examples

If you want a starting template instead of building from scratch, use official Stripe documentation and adapt from there. For reusable CSP patterns, https://csp-examples.com can help as a baseline, especially if you want route-specific examples or nonce-based templates.

For Stripe behavior and supported integration details, the official docs are the source of truth:

Final opinion

For Stripe.js, the best CSP is not the shortest one or the strictest-looking one. It’s the one that keeps your own scripts under control, grants Stripe only what it actually needs, and gets tested against real payment flows.

That usually means nonces, explicit Stripe sources, reporting, and a separate checkout policy if your app is large enough.

If your current policy looks polished everywhere except payments, that’s normal. Payments are where CSP stops being theoretical and starts being a negotiation.