Stripe.js, and the Payment Element and other Elements built on it, needs script-src https://js.stripe.com https://*.js.stripe.com, frame-src https://js.stripe.com https://*.js.stripe.com https://hooks.stripe.com and connect-src https://api.stripe.com. Those come from the Content Security Policy section of Stripe’s integration security guide. Card fields are iframes, so frame-src matters as much as script-src.
The policy
Content-Security-Policy: default-src 'self'; script-src 'self' https://js.stripe.com https://*.js.stripe.com; frame-src https://js.stripe.com https://*.js.stripe.com https://hooks.stripe.com; connect-src 'self' https://api.stripe.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'
The builder finds one low point: a <meta> tag ignores frame-ancestors, so send this as a header. *.js.stripe.com lets Stripe.js start its frames on separate origins for speed; it matches subdomains only, which is why https://js.stripe.com is listed as well.
3D Secure and redirects
Stripe asks for https://hooks.stripe.com in frame-src for payment methods that redirect, such as cards that may need 3D Secure, and iDEAL. Leave it out and the browser blocks that frame. Your frame-src governs only the frames your page creates; whatever loads inside Stripe’s frame answers to that frame’s own policy, so the list doesn’t grow with each bank.
What Checkout and the rest add
- Checkout:
https://checkout.stripe.cominscript-src,frame-srcandconnect-src, plusimg-src https://*.stripe.com. - Link:
https://link.com https://*.link.cominframe-srcandconnect-src, andimg-src https://*.link.com. - Address Element with your own Google Maps key:
https://maps.googleapis.cominscript-srcandconnect-src. - Fonts in Elements: a
cssSrcstylesheet, such as one from Google Fonts, has to be allowed byconnect-src. - Connect embedded components:
https://connect-js.stripe.cominscript-srcandframe-src, and a hash instyle-src. Stripe’s guide prints it bare,sha256-0hAh…=; in a header it needs its quotes,'sha256-…', or it reads as a host. The builder flags the bare form.
Gotchas
- Redirecting to hosted Checkout is a navigation, which CSP doesn’t restrict, with one exception. If a
<form>posts to your server and it answers with a 303 tocheckout.stripe.com, Chrome checksform-actionagainst that redirect too, soform-action 'self'blocks it. Addhttps://checkout.stripe.comtoform-action, or redirect from script. - Nonces. Stripe documents host allowlists only and says nothing about
'strict-dynamic'. Adding it under CSP Level 3 makes the Stripe.js<script>tag need your nonce, whileframe-srcandconnect-srcstay exactly as above:'strict-dynamic'only changes how scripts are trusted. Try that inContent-Security-Policy-Report-Onlyfirst. - Trusted Types. If you enforce them, Stripe’s guide gives a
defaultpolicy that lets script URLs fromhttps://js.stripe.comandhttps://*.js.stripe.comthrough.
In nginx
add_header Content-Security-Policy "default-src 'self'; script-src 'self' https://js.stripe.com https://*.js.stripe.com; frame-src https://js.stripe.com https://*.js.stripe.com https://hooks.stripe.com; connect-src 'self' https://api.stripe.com; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'" always;