CSP for Stripe.js and Stripe Checkout

script-src https://js.stripe.com https://*.js.stripe.com; frame-src https://js.stripe.com https://*.js.stripe.com https://hooks.stripe.com; connect-src https://api.stripe.com

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.com in script-src, frame-src and connect-src, plus img-src https://*.stripe.com.
  • Link: https://link.com https://*.link.com in frame-src and connect-src, and img-src https://*.link.com.
  • Address Element with your own Google Maps key: https://maps.googleapis.com in script-src and connect-src.
  • Fonts in Elements: a cssSrc stylesheet, such as one from Google Fonts, has to be allowed by connect-src.
  • Connect embedded components: https://connect-js.stripe.com in script-src and frame-src, and a hash in style-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 to checkout.stripe.com, Chrome checks form-action against that redirect too, so form-action 'self' blocks it. Add https://checkout.stripe.com to form-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, while frame-src and connect-src stay exactly as above: 'strict-dynamic' only changes how scripts are trusted. Try that in Content-Security-Policy-Report-Only first.
  • Trusted Types. If you enforce them, Stripe’s guide gives a default policy that lets script URLs from https://js.stripe.com and https://*.js.stripe.com through.

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;

Try it in the CSP Builder

everything that is fetched and has no directive of its own
scripts, inline and loaded
frames and iframes
fetch(), XMLHttpRequest, WebSocket and EventSource
<object>, <embed> and <applet>
the URLs a <base> element may point at
where forms may submit to
who may frame this page

A nonce has to be fresh for every response, so 'nonce-REPLACE_ME' is a placeholder: your server fills it in and puts the same value in the <script nonce=…> tag.

1 finding worst low

8 directives

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 same policy as a meta tag

<meta http-equiv="Content-Security-Policy" content="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'">

A meta tag only works in HTML, has to be in the <head>, and is read once: a header can be changed per response.

Findings · worst low

  • low A <meta> tag ignores frame-ancestors. If this policy goes into HTML, that directive is doing nothing there: send it as a header.

Every directive, in plain words

DirectiveWhat it governsSources
default-srceverything that is fetched and has no directive of its own
  • 'self' this origin only
script-src
→ default-src
scripts, inline and loaded
  • 'self' this origin only
  • https://js.stripe.com the host js.stripe.com over https
  • https://*.js.stripe.com any subdomain of js.stripe.com over https
frame-src
→ child-src → default-src
frames and iframes
  • https://js.stripe.com the host js.stripe.com over https
  • https://*.js.stripe.com any subdomain of js.stripe.com over https
  • https://hooks.stripe.com the host hooks.stripe.com over https
connect-src
→ default-src
fetch(), XMLHttpRequest, WebSocket and EventSource
  • 'self' this origin only
  • https://api.stripe.com the host api.stripe.com over https
object-src
→ default-src
<object>, <embed> and <applet>
  • 'none' nothing of this kind may load
base-urithe URLs a <base> element may point at
  • 'self' this origin only
form-actionwhere forms may submit to
  • 'self' this origin only
frame-ancestorswho may frame this page
  • 'self' this origin only

Checked against CSP Level 3: every directive, every source keyword, and the fallback chain that carries a directive to default-src. Nothing is uploaded, and no request is made to the site whose policy you paste.

Runs in your browser. Nothing you enter is sent anywhere.