CSP to Prevent Magecart: What It Stops and Misses

CSP to Prevent Magecart: What It Stops and Misses

A CSP to prevent Magecart works at exactly two moments: when the browser is asked to fetch something, and when the browser is asked to send something. Nothing else. A Content Security Policy is a list of origins the page is permitted to load from and talk to, delivered as an HTTP response header and enforced by the browser before the script runs or the request leaves the device. That is useful. It is also narrower than most advice suggests, and several documented skimmers were written to walk straight through a loose policy without triggering anything.

This piece sets out what the header actually blocks, what it cannot see, and what to put around it.

What a CSP does when a skimmer lands on your checkout

Magecart is a label for a method rather than a single crew, so there is no one signature to block. Different groups use different tooling, different hosting and different exfiltration routes. Your policy has to assume variety.

Most CSP guidance for checkouts obsesses over script-src. That is the entrance. Skimmers are stopped at the exit.

A working skimmer does not need to load anything visible. It reads the fields, collects the primary account number, expiry, CVV and billing address, then sends them to a server it controls. The send is the chokepoint. Which directive governs it depends on how the code is written:

  • connect-src covers fetch, XMLHttpRequest, WebSocket and navigator.sendBeacon. This is the common route.
  • img-src covers the oldest trick in the book: building an image element whose URL carries the stolen data in the query string. A policy with a tight script-src and a wide-open img-src for product images and CDNs leaves that channel fully open.
  • form-action covers where a form is allowed to post. Worth noting that form-action does not fall back to default-src. Neither does base-uri. If you do not write them, they are not set.
  • Browser navigation is not covered at all. The navigate-to directive was dropped and no major browser ships it, so code that redirects the browser to an attacker URL with data appended is outside CSP’s reach.

Which gives a blunt test of value. Open your checkout response headers. If there is no connect-src, or connect-src is https: or a wildcard, a skimmer’s beacon is permitted by your policy. The header is present. It is not doing the job it was installed to do.

The three entry routes, and which ones CSP can see

Skimmers reach a checkout by one of three routes, and the policy’s usefulness differs sharply across them. The pattern across the well-documented incidents is consistent on this point.

A compromised third-party supplier

Ticketmaster UK in 2018 is the standing example. Malicious code arrived inside a JavaScript module supplied by a third-party chat and support vendor, served from the vendor’s own domain. The ICO fined Ticketmaster UK £1.25m in November 2020 over the incident.

CSP helps here only if the supplier’s domain is not already on your allow list. If it is, and it almost always is, the policy approves the malicious file with no complaint. Your protection on this route comes from connect-src, because the supplier’s domain is approved for loading but should not be approved as a destination for arbitrary data.

Injected first-party code

The British Airways pattern from 2018 involved modification of a script already served from the airline’s own origin. The ICO issued a £20m penalty in October 2020.

A policy that trusts ‘self’ raises nothing at all in that scenario. The script is where it has always been. The hash, if you are not pinning, is unchecked. And if the exfiltration posts to a path on your own domain, with a server-side collector shipping it onwards, connect-src ‘self’ permits that too. Same-origin theft is invisible to origin-based policy.

A vulnerable or unpatched platform

In September 2020, Sansec reported close to 2,000 Magento 1 stores compromised over a single weekend, the largest campaign of its kind it had recorded. Code of that kind lands in the database, a CMS block or a template file. It is served by your own origin, inside your own trust boundary. The policy sees ordinary first-party script.

So where does CSP genuinely earn its keep? One place, and it is worth having: stopping the outbound request to a domain you never approved. Most skimmer kits hard-code a collection host. A tight connect-src, img-src and form-action means the card data is read but never leaves. That is not nothing. It is also not detection, and it does not remove the code.

Writing a CSP for a live payment page without breaking checkout

Checkout is the worst page on the site to break. Work in this order.

Start in report-only. Send Content-Security-Policy-Report-Only and leave it there long enough to cover the journeys that only happen occasionally: card-on-file repeat purchases, 3-D Secure step-up challenges, PayPal and wallet redirects, gift cards, address lookup, the refund and order-amend flows your support team uses. A full billing cycle is a reasonable floor. Two weeks of traffic that never once triggers a 3-D Secure challenge will produce a policy that fails the first time a bank asks for one.

Build the inventory while you collect violations. Every script, every pixel, every child tag fired by your tag manager, fonts, analytics beacons, session recorders, the payment provider’s library and its frames. Name the owner of each one and the business reason it is on a payment page. You need this for PCI DSS 4.0 anyway, so do it properly once.

A workable shape for a typical UK merchant with a hosted-fields payment provider looks like this:

Default-src ‘self’; script-src ‘self’ ‘nonce-r4nd0m’ https://js.yourpsp.com; connect-src ‘self’ https://api.yourpsp.com; frame-src https://js.yourpsp.com; form-action ‘self’; img-src ‘self’ https://cdn.yourstore.co.uk; base-uri ‘self’; object-src ‘none’; report-uri /csp-report

Two things undermine that policy faster than anything else. One is ‘unsafe-inline’ in script-src, which permits any inline block on the page, including the one an attacker wrote into a CMS field. The other is your tag manager, covered below.

Nonces or hashes for the inline scripts you cannot remove. A nonce is a random value generated per response and placed on both the header and the script tag. It demands a dynamic header, which rules it out if the page is served from a full-page cache without an edge worker to rewrite it. A hash is static and cache-friendly, but every change to that inline block, including a single space, means a new hash. Hashes suit inline code that is under version control and changes on release. Nonces suit templated pages that render per request. Pick per block, not per site.

Decide who reads the reports before you turn them on. Reports arrive as JSON POSTs and the volume is unpleasant at first. Browser extensions, injected toolbars, ad blockers, corporate proxies and the occasional ISP all generate violations that have nothing to do with you. Cross-origin blocked URLs are usually reduced to the origin, so you get the host and not the path. Filter aggressively, then look for the signal that matters: a violation naming a destination nobody recognises, appearing on the checkout and confirmation pages only, from more than one user agent. That is the shape worth paging someone about. The report-uri field is deprecated in favour of the Reporting API, so send both for now and check current browser support rather than trusting a blog post from 2019.

A fuller walkthrough of directive-by-directive tuning sits in the working checkout CSP setup, including the order of deployment for cached pages.

Closing the gaps: SRI, hosted fields and outside signals

Subresource Integrity is the direct answer to the supplier route. You add an integrity hash to the script tag, and the browser refuses to execute the file if the bytes do not match. Had SRI been applied to a pinned version of the Ticketmaster support module, the modified file would not have run. Generating those hashes without breaking checkout when suppliers update their scripts is covered in a separate piece.

The operational cost is real and rarely mentioned. When a supplier pushes an update, the hash stops matching and the script stops loading. That is correct behaviour, and it can take a widget down quietly while everything else on the page looks fine. So pin what is versioned and stable: a jQuery build, a payment library at a fixed version, a utility bundle you host yourself. Do not attempt to pin a tag manager, live chat or a personalisation engine that ships changes weekly. The practical boundaries are set out in the guide to pinning scripts you do not control.

Then there is the measure that changes the economics more than any header. If the card number is typed into an iframe served by your payment provider, script running in your page cannot read it, whatever else it does. Hosted payment fields are a configuration change, not a product purchase, and for many merchants they are also the route to SAQ A eligibility. The commercial argument usually lands faster than the security one.

What they do not cover: billing address, email, phone, order value, and the fake form drawn on top of the real one. An overlay skimmer renders its own card fields above the provider’s iframe, collects the details, then removes itself and lets the genuine payment proceed. Hosted fields do not stop that. Change detection does.

Keep the outside signals in view as well. Issuer fraud reports and acquirer common point of purchase alerts often arrive before any internal alarm, because the fraud shows up on cards, not on your dashboards. If your acquirer calls, treat it as a finding and not a nuisance, and start on confirming whether the checkout has been skimmed.

Mistakes that make a Magecart CSP look protective and do nothing

  • Allow-listing your tag manager. This voids the policy. Anyone with container access, an agency contractor, a marketing intern, or an attacker who phished one login, can publish arbitrary JavaScript from an approved source. The policy will not object. Neither will your monitoring.
  • Wildcard subdomains and bare https:. One forgotten wildcard on a large CDN permits a collection endpoint hosted on that CDN, which is a documented tactic.
  • Report-only forever. A report-only policy blocks nothing. Eighteen months in report-only usually means nobody will own the breakage risk, which is a governance problem wearing a technical costume.
  • Header on the homepage, not on the checkout. Policies set at the CDN or in a framework middleware frequently miss the payment and order-confirmation routes, which are often the routes handled separately. Confirmation pages carry order and customer data and get skimmed too.
  • CSP instead of patching. The Magento 1 campaign did not care about headers. Patch the platform, rotate admin credentials, enforce MFA on admin and the agency accounts, and remove old integration users.

Mapping the work to PCI DSS 4.0 requirements 6.4.3 and 11.6.1

Both requirements became mandatory on 31 March 2025, and both apply to the page that accepts cardholder data in the consumer’s browser.

Requirement 6.4.3 asks you to manage every script on the payment page: confirm each one is authorised, assure its integrity, and keep an inventory with written justification for each script. Requirement 11.6.1 asks for a mechanism that detects and alerts on unauthorised modification to the HTTP headers and the payment page content, running at least weekly or at a frequency your targeted risk analysis supports. The text is on the PCI Security Standards Council site; read 11.6.1 in full before you decide on frequency.

Neither requires a purchase. The inventory sheet covers authorisation and justification. SRI hashes cover integrity where pinning is viable. The CSP, with reporting enabled, covers the headers and gives you an alerting channel. A scheduled fetch of the rendered checkout, diffed against a known-good copy of the DOM and the response headers, covers modification detection. What matters to an assessor is evidence: the inventory in a shared sheet with owners and dates, the policy file in version control with its change history, and a documented review cadence triggered by supplier changes and platform upgrades. The detail of what each requirement expects is broken down in the walkthrough of 6.4.3 and 11.6.1.

A four-week deployment order

  1. Week one. Inventory the checkout. Open the page in a browser, list every script loaded, and delete anything nobody can justify in one sentence. A chat widget has no business on the page where card numbers are typed.
  2. Week two. Report-only policy live, violation endpoint receiving, triage set up. Run the odd journeys deliberately: 3-D Secure challenge, saved card, wallet, refund.
  3. Week three. Tighten connect-src, img-src and form-action to named hosts. Add SRI hashes to the versioned libraries. Leave the unpinnable ones documented as a known gap.
  4. Week four. Enforce. Add the scheduled header and content diff. Write down the review trigger: every supplier change, every platform upgrade, every new tag request.

The short version

A CSP to prevent Magecart blocks the exfiltration routes you wrote it for and nothing else. Connect-src, img-src and form-action do the real work; script-src alone will not stop a skimmer that is already inside your origin. It cannot see first-party injection, cannot see same-origin exfiltration, and cannot see a trusted supplier serving malicious code unless that supplier’s domain is also the destination. Pair it with SRI on the scripts that can be pinned, hosted payment fields so the PAN is never in your DOM, a weekly diff of the rendered checkout, and a patched platform. Then document the lot for 6.4.3 and 11.6.1. One header is not a defence. It is one layer, and a cheap one.

Frequently Asked Questions

Does a CSP actually prevent Magecart attacks?

It prevents some of the theft, not the compromise. A tight policy can stop a skimmer sending card data to a domain you never approved, which neutralises many off-the-shelf kits. It does not stop code being injected, does not stop exfiltration to an already-approved host, and does not stop a collector running on your own domain.

Which CSP directives matter most for stopping card skimming?

Connect-src, img-src and form-action. Those govern the outbound request carrying the data, whether it leaves by fetch, by an image URL, or by a form post. A strict script-src paired with a wildcard connect-src is close to worthless against a skimmer. Set base-uri and object-src as well, and remember that form-action and base-uri do not inherit from default-src.

How long should a checkout CSP stay in report-only mode?

Long enough to observe every payment journey at least once, which usually means a full billing cycle rather than a fortnight. After that, enforce. A report-only policy left running for months blocks nothing and gives false comfort to anyone reading the header.

Do I still need Subresource Integrity if I have a CSP?

Yes. CSP checks where a file came from. SRI checks whether the file changed. The Ticketmaster UK route in 2018 used a supplier domain that would have been allow-listed, so only an integrity check would have caught the modified module. Pin versioned libraries; accept that tag managers and live chat cannot be pinned and cover them with monitoring instead.

Does a CSP satisfy PCI DSS 4.0 requirements 6.4.3 and 11.6.1?

Partly. CSP reporting contributes to 11.6.1 by alerting on header and content changes, and the policy itself supports the authorisation side of 6.4.3. You still need the script inventory with written justification, an integrity assurance method such as SRI, and a change-detection check at a defined frequency. Using a CSP to prevent Magecart is a sound control to document, provided the inventory, the policy file history and the review cadence are evidenced alongside it.