A CSP allowlist for a checkout page is a list of the places that page may load code from, and the places it may send data to. It is written as a Content Security Policy header. Most merchants treat it as a header, write it once, and move on. Read plainly, that is the mistake: the allowlist is an inventory, and an inventory that nobody maintains stops describing reality within about a quarter.
This piece is about building that list and keeping it true. It assumes you already know what CSP is and have perhaps run a basic policy on a checkout page. What it adds is the part nobody documents: the decisions that make an allowlist worth having, and the four or five ways people quietly gut their own policy without noticing.
What a checkout allowlist actually is (and what it is not)
Three mechanisms get called “allowlisting”and they behave differently.
Origins, hashes and nonces
An origin entry names a host: scheme, hostname, optional port. The browser will load any script that host serves. A hash entry names the exact content of a script by its SHA digest. A nonce is a random value generated per response and placed on the script tags you intend to run; anything else inline is refused.
Checkout usually needs both kinds. Origins cover the third-party files you cannot control the bytes of. Nonces or hashes cover your own inline code, because a policy that permits ‘unsafe-inline’ permits the skimmer too. Skimmers are usually injected inline.
Why script-src on its own leaves the exfiltration route open
Approving script sources controls what runs. It says nothing about where data may go. A skimmer that arrives through an already-approved source can read the card fields and post them anywhere, because the policy never named permitted destinations.
Four directives share that job. connect-src governs fetch, XMLHttpRequest, sendBeacon, WebSockets and EventSource, which is how most modern skimmers beacon out. img-src matters because a one-pixel image request with the card number in the query string is still a working exfiltration channel. form-action stops a rewritten form posting to an attacker’s endpoint. frame-src controls which hosted payment iframes may be embedded, and so stops a convincing fake one being dropped in. A checkout policy that names script sources but not permitted destinations is half a policy.
The honest limit
An allowlisted supplier that gets compromised is still an allowlisted supplier. Ticketmaster UK is the clearest case: the malicious code arrived in 2018 through a trusted third-party customer support component embedded in the payment page, and the ICO fined the company £1.25 million in November 2020. Every browser on the site would have loaded that script happily under any origin-level allowlist written the week before.
First-party compromise sits outside the policy as well. In September 2020, security firm Sansec reported that around 2,000 Magento 1 stores were compromised over a single weekend through the platform itself, not through a supplier. Code injected into your own templates comes from your own origin. The allowlist narrows the field. It does not vouch for the code inside it.
Step 1: Inventory every source the payment page loads
Build the raw list from evidence, not from memory. Three sources, in this order.
- Open the live payment page in a browser, with DevTools on the Network panel, filtered to JS and XHR. Complete a test purchase. Record every distinct host, including the ones that appear only after the card fields render.
- Run a report-only policy of default-src ‘none’ for a few days and read the violation stream. Real traffic finds sources a desk test never will: consent-gated tags, geo-targeted pixels, legacy scripts that only fire for returning customers.
- Export your tag manager container and read the tag list, including paused tags and anything scheduled.
Expect the result to be longer than you guessed. A mid-sized UK checkout commonly carries somewhere between ten and thirty distinct script sources before any pruning: payment provider, analytics, consent manager, live chat, reviews widget, two or three remarketing pixels and the tag manager that loads half of them.
Sort them into tiers: first-party, payment provider, analytics, chat, A/B testing, marketing pixels. Then write two columns next to every row, and make them mandatory. Who owns this, by name. Why it exists on the payment page specifically.
That table is the point of the whole exercise. It is also, word for word, what PCI DSS 4.0 requirement 6.4.3 asks for: an inventory of scripts on the payment page, a written justification for each, and assurance that each is authorised. The allowlist and the 6.4.3 inventory are the same list viewed two ways. One is machine-readable, one has a named owner per line. Build them as separate projects and they will disagree within months, usually in the direction of the document being wrong.
Anything with no nameable owner is a removal candidate. Not a policy entry. There is always at least one.
Step 2: Decide what earns a place, six questions per script
Run every row through the same six questions before it goes anywhere near a header.
- Does it need to run on the payment page, or only on earlier funnel steps? Most analytics and remarketing answers the second. Conversion tracking can fire on the confirmation page, after the card data has gone.
- Can the card inputs be moved behind hosted payment fields, so the script never shares a DOM with them? That change does more for your risk than the policy will.
- Is the host a dedicated supplier origin, or a shared CDN? JsDelivr and unpkg will serve any package published to npm. Shared object storage hosts serve any customer’s bucket. Allowlisting an origin like that is close to allowlisting the open web.
- Does the supplier publish stable, versioned URLs that can be pinned with Subresource Integrity? Suppliers who ship rolling updates to a single filename cannot be pinned, and you should know that before you depend on them.
- Where does it send data, and which connect-src entry does that require? If adding one script means opening a destination you cannot describe, the script costs more than it looks.
- What does removal cost the business, and who signs that off? Get the name. A marketing lead who will not put their name to a pixel at checkout has just answered the question.
Step 3: Write the policy, with the traps marked
Pin as tightly as the directive allows. https://js.examplepay.com/v3/checkout.js is worth more than https://js.examplepay.com, which is worth far more than https://*.examplepay.com. Wildcard subdomains inherit every forgotten staging host and every abandoned marketing microsite the supplier ever stood up.
Keep ‘unsafe-inline’ and ‘unsafe-eval’ out. Both undo most of the benefit, because inline injection is the common skimmer delivery method and eval is how obfuscated payloads unpack themselves. The way out is a per-response nonce on your own script tags, or SHA hashes for the handful of inline blocks that cannot be moved to files.
Google Tag Manager is the usual hole. Allowlist the container and you have implicitly approved anything a marketing user can publish through it, including custom HTML tags, which inject arbitrary markup that your nonce never vetted. Two honest options: remove the container from the payment page entirely, or lock publishing rights, disable custom HTML, and treat every tag as an allowlist entry with an owner. A container audit is worth doing before you decide which.
A skeleton for a checkout using a payment provider’s hosted fields looks roughly like this:
- Default-src ‘none’
- Script-src ‘nonce-Rand0mPerResponse’ ‘strict-dynamic’ https://js.examplepay.com/v3/
- Connect-src ‘self’ https://api.examplepay.com
- Frame-src https://pay.examplepay.com
- Img-src ‘self’ data:
- Style-src ‘self’ ‘nonce-Rand0mPerResponse’
- Form-action ‘self’
- Base-uri ‘none’; object-src ‘none’; frame-ancestors ‘none’
- Report-uri /csp-report; report-to csp-endpoint
Two notes on that fragment. With ‘strict-dynamic’, browsers that support CSP Level 3 ignore the host entry and trust only nonced scripts and what they load; the named host stays as a fallback for older browsers, so keep it specific. And base-uri ‘none’ is not decoration. Without it, injected code can rewrite the document base and redirect every relative script path on the page. For the directive detail, the MDN reference on Content-Security-Policy is the reliable source.
Step 4: Prove it in report-only, on real traffic
Ship the policy as Content-Security-Policy-Report-Only first and leave it there long enough to see your actual customers, not your office. Two weeks across a full trading cycle is a reasonable floor; the 14-day report-only rollout covers the sequencing. Make sure the sample includes mobile Safari, in-app browsers and ad-blocked sessions, because those produce the oddest reports. Safari has historically honoured report-uri rather than the newer report-to, so send both.
Then read the stream properly. Most of the volume is noise. Violations with a blocked URI of chrome-extension, moz-extension or safari-extension are the customer’s own browser add-ons and cannot be fixed by you. Reports for about:blank or inline with no line number are frequently extension artefacts too. Genuine gaps look different: the same supplier host, from many users, at the same point in the flow.
Step 5: Enforce in stages, with a rollback written down
Switch enforcement on the payment page first, not site-wide. The payment page is the page that matters and the page with the smallest tag footprint, so the blast radius is small and the signal is clear. Watch conversion rate and payment-provider error rates for the first 48 hours, not just the violation count. Keep the rollback as a single config change with a named person able to make it out of hours.
Before you call it done, check the policy actually blocks what it claims to. In a staging copy of the live policy, add a snippet that attempts a fetch or a beacon to a domain not on the list, then confirm the browser refuses it and the violation report arrives at your endpoint. Testing the report pipeline matters as much as testing the block. Plenty of merchants discover months later that reports have been going nowhere since a certificate expired.
Step 6: Keep the allowlist alive
Launch is the easy part. Drift is what kills these policies.
Decide who approves a new source and write it into change control. In practice that means a marketing request to add a pixel at checkout goes through the same gate as a code deploy: named owner, stated purpose, source host, destination host, and a decision on whether it can live on an earlier page instead. Without that gate the list grows by accretion and the justification column goes blank.
Hash-pinning and allowlisting pull against each other, and anyone who has run both knows how. Subresource Integrity gives real assurance that the bytes have not changed, and a silent supplier update then breaks checkout on the next cache miss. The fix is not to drop the integrity attribute the first time it bites. Use versioned URLs where the supplier offers them, alert on integrity failures rather than discovering them through support tickets, and keep a documented re-hash process that a developer can run in twenty minutes. Suppliers who cannot provide a stable URL should be told that in writing, at renewal.
Tie every allowlist change to your payment page change detection under requirement 11.6.1. An approved change should produce an expected alert, and an alert with no matching change record should be treated as an incident until proven otherwise. That pairing is what turns two separate compliance tasks into one piece of work.
Review quarterly, and on triggers: platform upgrade, new payment provider, a supplier breach notice, any significant checkout redesign. Three signs the list has gone stale, all easy to spot: violation volume climbing month on month, entries for suppliers you stopped paying, and a justification column where more than one row says “legacy”.
The short version
A CSP allowlist for checkout is an inventory first and a header second. Capture every source from real traffic, not from memory. Name an owner and a reason for each one, which gives you requirement 6.4.3 in the same document. Name permitted destinations in connect-src, img-src and form-action, or a skimmer delivered through an approved source still gets the data out. Pin to paths, never to wildcard subdomains, and keep a shared CDN origin off the list unless you can pin the file. Prove the policy in report-only, enforce on the payment page first, and review it every quarter. The allowlist narrows what can run and where data can go. It does not promise that the code you approved is clean.
Frequently Asked Questions
What should go in a CSP allowlist for a checkout page?
Only the sources the payment page genuinely needs: your own origin, your payment provider’s script and iframe hosts, and any script with a named owner and a written reason for being on that specific page. Pin to exact hostnames and paths rather than wildcards, and use a nonce or hashes for inline code instead of permitting ‘unsafe-inline’. Analytics, chat and remarketing tags usually belong on earlier funnel steps or the confirmation page.
Does a CSP allowlist stop Magecart skimmers?
It stops the common delivery routes: inline injection, a script pulled from an unapproved host, and exfiltration to a domain the policy does not name. It does not stop a compromised supplier whose host you have already approved, which is exactly the pattern in the 2018 Ticketmaster UK breach. Treat the allowlist as one control among several, alongside hosted payment fields and payment page change detection.
Why does connect-src matter as much as script-src at checkout?
Script-src decides what runs; connect-src decides where data may be sent. Modern skimmers exfiltrate using fetch, XMLHttpRequest or sendBeacon, all of which fall under connect-src. Leave that directive unset and it falls back to default-src, which in many real-world policies is permissive enough to allow any destination.
How often should the checkout allowlist be reviewed?
Quarterly as a baseline, plus a review on any trigger: platform upgrade, new payment provider, checkout redesign, or a breach notice from a supplier on the list. Reviews between those points should be driven by the violation report stream, which tends to show drift before anything else does.
Can one allowlist satisfy PCI DSS 4.0 requirements 6.4.3 and 11.6.1?
One inventory can serve both, provided it is maintained properly. Requirement 6.4.3 asks for an inventory, written justification and authorisation for payment page scripts, which is the human-readable form of your allowlist. Requirement 11.6.1 asks for change and tamper detection on the payment page, so alerts should reconcile against the same list; both have been mandatory since 31 March 2025, and the current standard text is published by the PCI Security Standards Council.
