Search for a CSP script-src example for ecommerce and you get the same line in almost every result: script-src ‘self’ ‘unsafe-inline’ https:. That is a tutorial line. On a payment page it is a header that looks like a control and stops nothing, because it permits any script from any HTTPS origin and any inline block an attacker manages to write into the page.
What follows is four script-src policies written for checkouts, with the directives that generic examples leave out, and the PCI DSS 4.0 obligations they feed into. The four cover hosted payment fields, a Magento 2 checkout, a tag manager stack, and a thin iframe or redirect page.
What script-src controls on a payment page, and what it never will
Script-src answers one question: may the browser execute this JavaScript, given where it came from. Origin, nonce, or hash. That is the whole test.
It says nothing about what the script then does. An allow-listed origin is a trust decision about every file that origin will ever serve, including the file it serves next week after somebody else edits it.
Two incidents make the point better than any explanation. Ticketmaster UK, disclosed in June 2018: the skimming code arrived inside a customer support chat script supplied by Inbenta. A policy reading script-src ‘self’ https://chat-supplier.example would have loaded it without complaint. British Airways, over roughly 21 August to 5 September 2018: the malicious code sat in a modified JavaScript file served from the airline’s own infrastructure. Every policy containing ‘self’ permits that. The ICO fined British Airways £20 million in October 2020 over the breach.
Neither attack needed an origin you had not approved. Both used one you had. That is the argument for pinning third-party files with Subresource Integrity hashes alongside the allow-list, rather than treating the allow-list as the finish line.
The exfiltration half, which most examples omit
Assume a skimmer is already running. It now has to get the card number out. It will use fetch or XHR to an attacker endpoint, a WebSocket, a 1×1 image with the data in the query string, or a cloned form posting somewhere else.
Script-src stops none of that. connect-src governs fetch, XHR, sendBeacon and WebSockets. img-src governs the beacon trick. form-action governs where a form may post. On a payment page those three deserve as much attention as script-src, and almost no published example includes them.
CSP also sits beside, not instead of, the other work. An unpatched platform means the attacker edits the files your own policy already trusts. Sansec counted 1,904 Magento 1 stores compromised over a single weekend in September 2020, long after that version left support. No header helps there.
Inventory first, before you write a single directive
Open the checkout in a clean browser profile. Record every script in the Network panel, then query the DOM for script elements after the page has settled, because tag managers inject at runtime and the two lists rarely match.
For each script, write down the origin, the owner, what it does, and why it needs to be on the page where card numbers are typed. That record is requirement 6.4.3 of PCI DSS 4.0, which asks for an inventory of payment page scripts with written justification for each, a method of authorising them, and a method of assuring their integrity. Both 6.4.3 and 11.6.1 have been mandatory since 31 March 2025. The PCI Security Standards Council document library holds the current text.
Then delete. Session recording, heatmaps, A/B testing, live chat, affiliate pixels: most of them exist on the checkout because nobody scoped them to anything narrower. The shortest policy is the one with the fewest third parties left in it, and removing a script is free, permanent and needs no maintenance. A chat widget has no business on the page where card numbers are typed.
Example 1: report-only script-src for a checkout using hosted payment fields
Start here if the card fields are iframes served by your provider. Send this as Content-Security-Policy-Report-Only first, on the checkout path only.
- default-src ‘self’. A floor, so anything you forget to name falls back to first-party.
- script-src ‘self’ https://js.provider.example. First-party bundles plus the provider’s JavaScript origin. Nothing else.
- frame-src https://js.provider.example https://hooks.provider.example. Hosted fields are iframes. Omit this and the card inputs do not render, which is the single most common cause of a “CSP broke our checkout”ticket.
- connect-src ‘self’ https://api.provider.example. Tokenisation is an API call from the browser. Block it and payment fails silently at submit.
- img-src ‘self’ data: https://js.provider.example. Card scheme logos, and data: for inline SVG if your theme uses it.
- form-action ‘self’. The order form posts to your own origin and nowhere else.
- frame-ancestors ‘none’. Nobody frames your checkout.
- base-uri ‘none’. Stops an injected base tag rewriting every relative script path in one line.
- object-src ‘none’.
Take the provider hostnames from the provider’s current integration documentation, not from a blog post. They change, and a stale list fails in production rather than staging.
Reporting: set a Reporting-Endpoints header and a report-to directive for current browsers, and keep report-uri alongside it for older ones. Sending to both costs nothing.
A normal week of reports is mostly noise. Browser extensions injecting script from chrome-extension: and moz-extension: origins, antivirus products rewriting pages, translation tools, inline blocks reported with no line number. You cannot control any of it and you should not try. What matters is an HTTPS origin you do not recognise, appearing on the checkout path, reported by more than one user agent. That is the signal. A staged report-only rollout is where the triage habit gets built.
Example 2: enforced nonce-based script-src for a Magento 2 checkout
Magento is where most merchants give up and write ‘unsafe-inline’. The checkout is full of inline Knockout bindings and RequireJS configuration, the policy breaks, and the keyword goes in to make the error stop. At that point the policy permits any inline script an attacker injects, which is precisely how a skimmer runs. The header is then decoration.
Use nonces instead. The Magento_Csp module has shipped with the platform since 2.3.5, with origin allow-lists declared in csp_whitelist.xml and separate report-only and restrict modes per area. Adobe Commerce 2.4.7 added a nonce provider for inline scripts. Check which version your store actually runs before planning the work, because the approach differs.
- script-src ‘nonce-{random}’ ‘strict-dynamic’ ‘unsafe-inline’ https:. The nonce is generated per request. ‘strict-dynamic’ lets a nonced loader such as RequireJS pull in its dependencies without you listing every bundle. The trailing ‘unsafe-inline’ and https: are ignored by any browser that understands nonces and ‘strict-dynamic’, and act as fallback for ones that do not. That is the only context in which ‘unsafe-inline’ belongs in a modern policy.
- script-src-elem is worth setting explicitly if you need different handling for script elements and event handlers.
- connect-src ‘self’ https://api.provider.example, plus any payment or fraud endpoint the checkout genuinely calls.
- form-action ‘self’ https://3ds.provider.example. Three-D Secure step-ups often post off-site. Test this one with a real challenge flow.
- img-src ‘self’ data: https://cdn.yourstore.example.
- base-uri ‘none’, object-src ‘none’, frame-ancestors ‘none’.
Some inline blocks cannot be nonced, usually ones generated by a third-party module. Hash them with sha256 and list the hash in script-src. Accept the maintenance cost honestly: one byte changes in that block after a module update and the hash fails. Record which hashes belong to which module, and re-check them as part of every deployment that touches checkout.
Roll out in order. Report-only on the checkout path. Enforce on the checkout path. Only then widen to the rest of the store. Doing it the other way round means your first enforcement failure happens on the page that takes the money.
Example 3: tag manager and analytics without a blank cheque
Allow-listing a container origin is a decision to trust whatever anyone publishes into that container later, including the marketing contractor with edit access and a custom HTML tag. CSP cannot tell a legitimate tag from a malicious one. They arrive from the same origin and run with the same permission.
The permissive version, for pages that are not checkout:
- script-src ‘self’ ‘nonce-{random}’ ‘strict-dynamic’, with the container loader carrying the nonce. Tags injected by the container inherit trust.
- connect-src ‘self’ https://analytics-endpoint.example. Keep this explicit even here, because it is the directive that limits where measurement data goes.
The tightened version, served only on /checkout and its sub-paths:
- script-src ‘self’ ‘nonce-{random}’, with no ‘strict-dynamic’ and no container origin at all.
- Measurement for the purchase event moves server-side, or fires from the order confirmation page rather than the payment step.
That is the position worth defending: the container does not load on the payment page. If the business will not accept that, the fallback is container access control, two-person review on tag publishing, and version-history alerts, which is a process control rather than a technical one. Auditing what is already in the container usually finds tags nobody can account for.
Use ‘strict-dynamic’ where a loader legitimately pulls a changing set of files. Use an explicit origin list where the set is small, stable and known, because an explicit list still tells you something when a report arrives.
Example 4: the minimal policy for an iframe or redirect payment page
If the payment page’s only job is to host the provider’s iframe, the policy gets very short.
- default-src ‘none’
- script-src ‘none’, or ‘self’ with a single hashed inline block if the page needs one
- frame-src https://pay.provider.example
- style-src ‘self’
- img-src ‘self’
- form-action ‘none’. The page submits nothing itself, so an injected fake payment form has nowhere to post.
- frame-ancestors ‘none’
This is the cheapest risk reduction available, and moving card entry off your own DOM does more than any header. Be clear about the limits, though. It protects the payment step and nothing before it. Address capture, email capture, basket and order confirmation are all still yours, and an attacker who can inject into those pages can harvest personal data, or redirect the customer before they ever reach the provider.
The scope argument is also less settled than vendors suggest. PCI DSS 4.0 brought iframe and redirect merchants into SAQ A, and the Council’s revised SAQ A published on 30 January 2025 removed requirements 6.4.3 and 11.6.1 from that questionnaire, replacing them with an eligibility criterion about the site not being susceptible to script attacks affecting the payment page. Confirm the current position with your acquirer rather than assuming. Either way, change detection on the page that frames the iframe is still work worth doing.
What breaks, and keeping the policy alive
The failures are predictable. A bare https: source permits every HTTPS host on the internet. A wildcard subdomain permits whatever a compromised subdomain serves. ‘unsafe-eval’ left behind by one legacy plugin re-enables string-to-code execution for every script on the page. And CSP delivered by meta tag cannot run in report-only mode and cannot carry frame-ancestors, so any rollout that starts safely needs a real HTTP header.
Test on staging with genuine third-party traffic, not a stripped environment. Include mobile Safari and Chrome, Apple Pay and Google Pay buttons, 3DS challenge screens, and any address lookup service. Wallet buttons are the usual surprise, because they load late and from origins nobody inventoried.
Requirement 11.6.1 asks for a mechanism that alerts personnel to unauthorised change to payment page scripts and to the HTTP headers received by the consumer browser, evaluated at least weekly or at a frequency set by a documented risk analysis. Your own CSP header is so in scope for monitoring. Violation reports plus header monitoring plus a script inventory can form a reasonable evidence trail, as set out in what 6.4.3 and 11.6.1 actually ask for. It only counts if a named person reads the reports and records what was done about them.
Set the review routine before you need it. One owner for the allow-list. Re-check SRI hashes on every supplier release. Re-run the script inventory after any checkout deployment and at least quarterly. A fuller walkthrough of the headers, ordering and rollout sits in this working checkout CSP setup.
The short version
Script-src decides where JavaScript may come from, not what it does once it runs. Inventory the scripts, delete what you can, then write a checkout-specific policy with nonces rather than ‘unsafe-inline’, and set connect-src, img-src and form-action to close the exfiltration routes. Pin third-party files with SRI. Run report-only first, read the reports, and keep patching, because an attacker who can edit a file you already trust does not care about your header.
Frequently Asked Questions
What should script-src contain on an ecommerce checkout page?
‘self’, a per-request nonce for the inline blocks your platform generates, and the smallest possible list of payment provider origins taken from the provider’s current documentation. No wildcards, no bare https:, no ‘unsafe-eval’. Pair it with connect-src, img-src, form-action, base-uri and frame-ancestors, because script-src alone does not stop stolen data leaving the page.
Can I use ‘unsafe-inline’ in script-src if my platform injects inline scripts?
Only as the ignored fallback that sits behind a nonce and ‘strict-dynamic’ for older browsers. Used on its own, it permits any inline script an attacker injects, which is the exact behaviour a skimmer relies on. Magento 2 pushes developers towards it because of inline Knockout and RequireJS code; use the platform nonce provider, and hashes for the blocks you cannot nonce.
Does a CSP script-src policy stop a Magecart skimmer?
It stops the ones that load code from an origin you never approved, and that is a large share of them. It does not stop code served from an origin already on your list, as at Ticketmaster UK in 2018, or code written into your own files, as at British Airways the same year. Treat it as one layer alongside SRI, patching and change detection.
Should the checkout have a different CSP from the rest of the store?
Yes. Marketing and personalisation scripts that are acceptable on a product page have no justification on the page where card numbers are typed, and a single site-wide header always ends up as loose as its loosest page. Serve a stricter policy on the checkout path and enforce it there first.
Does a content security policy satisfy PCI DSS requirements 6.4.3 and 11.6.1?
It contributes to both without covering either on its own. The allow-list and the inventory behind it support 6.4.3, and monitored violation reports support 11.6.1, but 11.6.1 also expects detection of unauthorised change to the payment page content and HTTP headers as received by the browser, at least weekly or at a documented risk-based frequency. Keep records of who reviewed what and when.
