A CSP report-only checkout rollout is the one way to put a Content Security Policy on a live payment page without risking a single order. The browser reads the policy, works out what it would have blocked, sends you a report, and then loads the resource anyway. Nothing breaks. You find out what your checkout actually loads before you take away its permission to load it.
This is written as a fortnight of work, with tasks per day, because the report-only phase is where most rollouts stall. Teams enable the header, watch a few thousand reports arrive, and quietly leave it there for eight months. The policy that was meant to stop a skimmer is still logging to an endpoint nobody reads.
Why report-only is the only safe way to start on a live payment page
The Content-Security-Policy-Report-Only header is evaluated exactly like the enforcing header, with one difference: violations are recorded and reported, never blocked. The MDN reference for the header sets out the behaviour and the reporting fields. You get the full picture of what an enforced policy would do, with no effect on the customer.
The failure it prevents is specific and expensive. An enforced script-src that omits your payment provider’s tokenisation script will not produce an error page. The form renders. The customer types a card number. The script that is supposed to exchange that number for a token never loads, so the field is captured and nothing is authorised. Your conversion rate drops and the logs look normal.
Checkout is the worst page to learn CSP on, which is precisely why it is the page worth starting with. Content Security Policy for a checkout page is not the same job as a policy on your blog: the directive set is narrower, the tolerance for error is zero, and the payload you are protecting is a card number typed in real time. An observing header on the payment path is worth more than an enforced header everywhere else.
The honest limit: report-only observes, it does not defend. It is a two-week phase, not a destination.
Before day one: inventory what your checkout actually loads
Open your own checkout in a browser with the network panel recording. Put a real item in a basket, hard-refresh, and walk the whole flow to the point of payment. Record every origin that appears, grouped by type: scripts, images, fonts, XHR and fetch calls, and frames.
Sort what you find into four buckets.
- Your own domain. Theme JavaScript, bundles, inline handlers.
- The payment provider. Usually a script origin, an API origin for tokenisation calls, and one or two frame origins if you use hosted fields.
- Marketing and analytics. Tag manager container, ad pixels, consent tool, session recorder.
- Anything injected at runtime. Whatever the tag manager loads after the page has rendered, which is not necessarily what it loaded yesterday.
That fourth bucket is the blind spot. A container gives a marketing user the ability to add a new origin to your payment page without a deployment, a review or a ticket. Your inventory is accurate on the day you wrote it and provisional after that.
While you are already looking at every script on the page, write down two more columns: who in the business asked for it, and why it is on the payment page. That is the artefact PCI DSS 4.0 requirement 6.4.3 asks for, and the authorisation, justification and integrity obligations under 6.4.3 and 11.6.1 have been mandatory since 31 March 2025. Produced here it costs you an extra column. Produced separately it costs a week.
Writing the first report-only policy for checkout
Start with five directives, not thirty
Skimming is a loading problem and a sending problem. Five directives cover both: script-src, connect-src, frame-src, form-action and base-uri. Add object-src ‘none’ because nothing on a checkout needs plugins. Leave images, fonts and styles out of the first draft; they generate volume and tell you little.
A workable starting shape for an e-commerce checkout, using a named provider as the example, looks like this (check your own provider’s current documentation for its live domains, which do change):
- script-src ‘self’ ‘nonce-RANDOM’ https://js.stripe.com
- connect-src ‘self’ https://api.stripe.com
- frame-src https://js.stripe.com https://hooks.stripe.com
- form-action ‘self’
- base-uri ‘self’
- object-src ‘none’
- report-uri /csp-report-endpoint; report-to csp-endpoint
Connect-src and form-action matter as much as script-src
First drafts almost always police scripts and ignore the exits. Harvested card data has left payment pages as a query string on an image request, as a fetch to a lookalike domain, and by rewriting the form’s target so the browser posts the whole field set to the attacker as well. A script-src-only policy watches the front door while the data leaves by the window.
base-uri is the cheap one people skip. An injected <base> tag can re-point every relative URL on the page, including the one your form posts to, without touching a single script origin.
Nonces, hashes and the ‘unsafe-inline’ trap
Origin allowlists are the easy start and the weak finish. A nonce per request, or a hash per inline block, is what actually constrains what runs. If you put ‘unsafe-inline’ in a report-only policy to quieten the noise, you have removed the one thing a skimmer needs permission for, and the fortnight produces nothing worth enforcing. Keep it out and accept the inline reports as work to be done.
Reporting and scope
Set both report-uri and the newer report-to with a Reporting-Endpoints header, because browser support differs and you want reports from all of them. A self-hosted collector is fine if someone owns it; a managed reporting service is worth the money mainly because it groups and de-duplicates for you. Whichever you choose, send the header only on the basket, checkout and payment paths. Covering the whole site in week one buries the payment page in reports from the homepage carousel.
Days one to five: reading the reports without drowning in noise
Day one: confirm the pipeline, read nothing
Deploy the header. Place one deliberate violation, such as a script from a domain you have not allowed on a staging page, and confirm the report arrives, is stored, and is still there tomorrow. An endpoint that accepts POSTs and discards them is a common and silent failure.
Days two and three: group before you interpret
A violation report gives you document-uri (the page), violated-directive and effective-directive (which rule), blocked-uri (what was stopped) and often source-file and a line number. Expect blocked-uri to disappoint. Cross-origin redirects are stripped back to the origin to avoid leaking paths, and inline code arrives as the bare string inline, which tells you a directive was breached and almost nothing about where.
Group by blocked-uri first, always. A busy checkout can generate several thousand reports in a day that collapse to eight or nine distinct origins once grouped: typically two belonging to the payment provider, three marketing tags, and the rest browser extensions. Read the grouped list. Never the raw feed.
Days four and five: filter the predictable noise
The first forty-eight hours belong to the customer’s browser, not your site. Password managers, shopping extensions, injected antivirus scripts, and the chrome-extension:// and safari-extension:// schemes will dominate the volume. Teams that do not filter these conclude CSP is unworkable and stop. The signal is the handful of real origins underneath.
What should make you stop and look: an origin nobody recognises, appearing in a connect-src violation, at the step where card fields are being filled. Supplier churn looks like a new subdomain of a company already on your list, appearing on every page load. Exfiltration looks like a single unfamiliar host that only fires once the form has data in it.
Days six to twelve: tightening, retesting and pinning what you cannot control
The loop
Add one legitimate origin. Run a complete test order on desktop and on a mobile browser. Check the grouped report volume for that origin drops to zero. Repeat. Resist the urge to add six origins at once, because when something still reports you will not know which addition was wrong.
Narrow the path, not just the domain
Allowing an entire CDN or a shared hosting origin re-opens the door you are trying to shut. CSP permits paths, so allow the directory the supplier’s script lives in rather than the whole host. Ticketmaster’s UK compromise, disclosed in June 2018, came in through a third-party supplier’s chatbot script, which is exactly the sort of origin a checkout allowlist would have contained as legitimate. Naming the domain is not the control. Narrowing the path and pinning the file is closer to one.
Pair the policy with integrity hashes, and plan for the break
CSP says where a script may come from. Subresource Integrity pins the exact bytes that are allowed to run. The two pull against each other in practice: when a supplier legitimately updates their file, the hash stops matching and the script stops loading. Decide now who watches for that failure and how fast the hash gets updated. Otherwise someone deletes the integrity attribute under pressure on a Friday afternoon, and the pin is gone for good.
Decide what should not be there at all
The fortnight’s most useful output is often a removal list. A chat widget, an A/B testing tag and a review script have no business on the page where card numbers are typed. Move them to the product and basket pages. Every origin you delete is one you never have to justify, hash, monitor or allow.
If you use hosted payment fields served from the provider’s iframe, the card inputs sit outside the reach of your own scripts, which shrinks what a skimmer on your page can read. It does not cover the surrounding page. A fake overlay form, a tampered confirmation step, or an attacker swapping the iframe’s source are all still yours to handle, and frame-src is what constrains the last of those.
Days thirteen and fourteen: switching to enforcement without losing orders
Go when all four are true: seventy-two hours with no new legitimate origin appearing, a clean completed test order on desktop and mobile, a named business owner against every allowed source, and a rollback written down.
Serve both headers at once. The enforcing Content-Security-Policy carries the known-good set you have just proved. A stricter Content-Security-Policy-Report-Only sits alongside it and keeps watching, which is how you tighten further without ever experimenting on live card entry. The two-header pattern is permanent, not transitional.
Sequence the rollout: staging store first, then a percentage of live traffic if your platform or CDN supports splitting, then everything. The rollback plan is one line long and must be true: how the header is removed in under ten minutes, and which named people can do it at 2am on a Sunday. If that is one developer on holiday, you are not ready.
Keep the reporting endpoint running after the switch. The same pipeline that told you what your checkout loaded now tells you when it changes, which is the evidence requirement 11.6.1 expects for payment-page change detection.
Mistakes that make a report-only rollout useless
- Nobody owns the switch. The header sits in report-only for months, logging politely, blocking nothing. Put a date in a calendar with a name against it.
- The endpoint is never verified. Reports are accepted, never retained. Check on day one and again on day ten.
- The policy was copied from a blog post. Complete with ‘unsafe-inline’ and a wildcard, then declared protection.
- Only the basket page was tested. The provider’s final-step and 3-D Secure scripts load after payment is submitted. If you never complete a payment, those origins never appear in your reports, and they are the ones that break at enforcement.
- Report-only is treated as detection. It is not.
That last one deserves its own paragraph. A skimmer injected into your own first-party JavaScript sits inside an allowed origin and produces no violations whatsoever. Sansec reported in September 2020 that roughly 2,000 Magento 1 stores were compromised in a single weekend through the unpatched platform itself, which is precisely that shape of attack: hostile code, served from self, perfectly compliant with the policy. A policy allowing your own domain reports nothing. The routes into high-profile card skimming incidents split roughly between supplier scripts and platform compromise, and CSP is strong against the first and almost blind to the second.
The short version
Inventory the payment page with the network panel, five directives, both report headers, an endpoint you have proved retains data. Group by blocked-uri, discard the extension noise, add origins one at a time with a full test order after each. Delete the scripts that should never have been there. Flip to enforcement on day fourteen with a stricter report-only header still watching alongside, and keep the endpoint alive afterwards. The inventory you produce is your 6.4.3 paperwork. For the directive-by-directive detail behind the policy itself, the working checkout CSP setup covers what each line does and why.
Frequently Asked Questions
What does CSP report-only mode do on a checkout page?
The browser evaluates your policy against everything the page loads, records what would have been blocked, sends a JSON report to your endpoint, and then loads the resource anyway. No script is stopped and no payment is affected. On a checkout it gives you an accurate list of every script, API call, frame and form target the page depends on, before you take away any permissions.
How long should a CSP report-only checkout rollout run before you enforce?
Two weeks is enough for most stores, and the calendar matters less than the criteria: seventy-two hours with no new legitimate origin, a completed test payment on desktop and mobile, and a named owner for every allowed source. Extend it if a supplier release or a peak trading period falls inside the window. Do not extend it indefinitely, because report-only blocks nothing.
Where should checkout CSP violation reports be sent?
To an endpoint that groups, de-duplicates and retains, and that someone is responsible for reading. A self-hosted collector on a path you control works if it is monitored; a managed reporting service saves you building the grouping yourself. Set both report-uri and report-to for browser coverage, and keep the endpoint running after you switch to enforcement.
Will a report-only CSP slow down or break my payment page?
It will not break it. Report-only never blocks, so card entry and authorisation behave exactly as before. The only cost is the volume of report POSTs leaving customers’ browsers, which is why you scope the header to the checkout paths and give the endpoint something that can absorb a few thousand requests a day.
Does a CSP on checkout satisfy PCI DSS 4.0 requirements 6.4.3 and 11.6.1?
Not by itself. Requirement 6.4.3 asks that every script on the payment page is authorised, justified and has its integrity assured, and the inventory and hash-pinning work in this fortnight goes a long way towards that record. Requirement 11.6.1 asks for detection of unauthorised changes to the payment page headers and content, which needs ongoing monitoring rather than a policy alone. Both have been mandatory since 31 March 2025, and the published PCI DSS v4.0.1 standard from the PCI Security Standards Council is the authority on how each is assessed.
