Stripe Elements is one of those integrations that looks simple until CSP gets involved. The moment you lock down script-src, frame-src, and connect-src, your payment form can go from “works great” to “card field is blank and nobody knows why.”
I’ve seen teams waste hours debugging this because Stripe is doing exactly what it should do: loading payment UI in isolated iframes, talking to Stripe APIs, and sometimes pulling in supporting assets from Stripe-controlled domains. CSP needs to allow that behavior without turning your policy into a giant wildcard mess.
Here’s the practical comparison guide I’d want on hand before shipping Stripe Elements on a production site.
The core problem with Stripe Elements and CSP
Stripe Elements is not just a script include. It usually needs:
- Stripe JavaScript from
js.stripe.com - embedded frames for card fields and payment UI
- network calls to Stripe APIs
- sometimes styles and images from Stripe domains depending on the flow
That means a strict CSP that only allows 'self' will break it.
A typical locked-down baseline might look like the real 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-ZjVlMDhjNjItN2MwZC00MzgwLTg4ZWItOGYzOTBhNGQ5MGNi' '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'
That’s a decent strict policy for analytics and consent tooling. It will also break Stripe Elements immediately because Stripe isn’t allowed in script-src, frame-src, or connect-src.
The three common CSP approaches
When teams add Stripe Elements, they usually end up in one of these buckets.
Option 1: Add Stripe domains directly to your existing allowlists
This is the most common approach, and usually the best starting point.
Example
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{ .CSPNonce }}' 'strict-dynamic' https://js.stripe.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.stripe.com https://r.stripe.com;
frame-src 'self' https://js.stripe.com https://hooks.stripe.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
You may also need additional Stripe endpoints depending on product usage and region, but this is the usual shape for Elements.
Pros
- Easy to understand
- Works well with an already strict policy
- Keeps Stripe permissions scoped to only the directives that need them
- Good fit for teams already using nonces and
strict-dynamic
Cons
- You need to maintain the domain list as your Stripe integration changes
- It’s easy to miss one directive and end up with flaky failures
- Different Stripe products can need slightly different allowances
My take
This is the default choice I’d recommend. It’s explicit, reviewable, and doesn’t blow a hole in your policy. If something breaks, you can reason about it.
Option 2: Loosen CSP with broader wildcards for Stripe
Some teams go with a broad policy because they want fewer support tickets during rollout.
Example
Content-Security-Policy:
default-src 'self';
script-src 'self' 'unsafe-inline' https://*.stripe.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self' https://*.stripe.com;
frame-src 'self' https://*.stripe.com;
object-src 'none';
base-uri 'self';
Pros
- Fastest route to “it works”
- More resilient to minor Stripe infrastructure changes
- Lower chance of obscure integration breakage
Cons
- Too permissive for most production environments
- Wildcards make reviews harder
'unsafe-inline'inscript-srcis a bad trade unless you truly have no alternative- Broad
*.stripe.comallowances may permit more than your app actually needs
My take
I avoid this unless I’m temporarily debugging or dealing with a legacy app that I can’t clean up yet. Broad wildcards have a habit of becoming permanent. Once that happens, your CSP stops being a real control and becomes security theater.
Option 3: Separate CSP by route or page type
This is the mature option for apps with different risk profiles across pages.
Your marketing pages don’t need Stripe. Your checkout page does. So don’t give every page Stripe permissions.
Example
For most pages:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{ .CSPNonce }}' 'strict-dynamic';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
connect-src 'self';
frame-src 'self';
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
For checkout pages:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{ .CSPNonce }}' '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;
object-src 'none';
base-uri 'self';
frame-ancestors 'none';
Pros
- Best least-privilege model
- Keeps non-payment pages cleaner and safer
- Easier to audit what payment pages are allowed to do
Cons
- More deployment complexity
- Requires your framework or CDN to support route-level headers cleanly
- Can be annoying in SPAs if checkout is rendered client-side under one shell
My take
If you can do route-specific CSP, do it. This is the cleanest production setup. I especially like it for ecommerce and SaaS billing flows where the checkout path is well defined.
Which directives usually matter for Stripe Elements
These are the ones that trip people up most often.
script-src
You’ll need Stripe’s loader script, usually from:
https://js.stripe.com
If you use nonces and strict-dynamic, keep them. Don’t throw away a good setup just because you’re adding Stripe.
If you want a refresher on how script-src works, the directive reference at https://csp-guide.com is useful.
frame-src
Stripe Elements relies heavily on frames. If card fields don’t render, this is one of the first places I look.
Typical values include:
https://js.stripe.com
https://hooks.stripe.com
connect-src
Stripe needs to talk to its backend APIs.
Typical values include:
https://api.stripe.com
https://r.stripe.com
Depending on your implementation, reporting, and fraud tooling, the exact endpoints can vary.
img-src
Usually https: is enough here if your site already permits remote images broadly. If you run a very strict image policy, Stripe-related assets may need to be allowed explicitly.
style-src
Stripe Elements itself is usually less of a style-src headache than many UI libraries, but your own checkout code may still rely on inline styles or framework-generated style tags. Don’t blame Stripe for your CSS pipeline problems.
Adapting the headertest.com policy for Stripe
Starting from the real header, here’s a practical version with Stripe added while preserving the original structure:
Content-Security-Policy:
default-src 'self' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com;
script-src 'self' 'nonce-ZjVlMDhjNjItN2MwZC00MzgwLTg4ZWItOGYzOTBhNGQ5MGNi' 'strict-dynamic' https://www.googletagmanager.com https://*.cookiebot.com https://*.google-analytics.com https://js.stripe.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 https://api.stripe.com https://r.stripe.com;
frame-src 'self' https://consentcdn.cookiebot.com https://js.stripe.com https://hooks.stripe.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none'
That’s the kind of change I’d make first before widening anything further.
Sample Stripe Elements page
Here’s a stripped-down example that matches the CSP shape above.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Checkout</title>
<script nonce="{{ .CSPNonce }}" src="https://js.stripe.com/v3/"></script>
</head>
<body>
<form id="payment-form">
<div id="card-element"></div>
<button type="submit">Pay</button>
</form>
<script nonce="{{ .CSPNonce }}">
const stripe = Stripe('pk_test_12345');
const elements = stripe.elements();
const card = elements.create('card');
card.mount('#card-element');
document
.getElementById('payment-form')
.addEventListener('submit', async (e) => {
e.preventDefault();
// Confirm payment with your server/client flow here
});
</script>
</body>
</html>
Common mistakes
Forgetting frame-src
This is probably the number one issue. Developers allow js.stripe.com in script-src and assume they’re done. They’re not.
Adding Stripe to default-src and calling it a day
Don’t rely on default-src as a catch-all if you already use specific directives. Once script-src, frame-src, or connect-src are present, those explicit directives take over.
Dropping strict-dynamic because Stripe broke
That’s usually the wrong fix. Keep your nonce-based model if you already have it.
Allowing all of https:
I get the temptation, especially under launch pressure. It solves the immediate breakage and creates a future audit headache.
My recommended setup
If you want the short version:
- Use route-level CSP if your app can support it
- Keep nonces and
strict-dynamic - Add only the Stripe domains required for
script-src,connect-src, andframe-src - Start narrow, then expand only if real CSP violation reports show a missing dependency
For most Stripe Elements integrations, this is the sweet spot:
Content-Security-Policy:
default-src 'self';
script-src 'self' 'nonce-{{ .CSPNonce }}' 'strict-dynamic' https://js.stripe.com;
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.stripe.com https://r.stripe.com;
frame-src 'self' https://js.stripe.com https://hooks.stripe.com;
frame-ancestors 'none';
base-uri 'self';
form-action 'self';
object-src 'none';
If you need the exact Stripe requirements for your payment flow, check the official docs at https://docs.stripe.com. For CSP directive behavior, https://csp-guide.com is handy when you need to sanity-check how a directive actually applies.
Stripe Elements and CSP can absolutely coexist. You just need to be deliberate about what Stripe needs, and resist the usual urge to “fix” it with a wildcard.