Here is a Magecart attack explained in one line: it takes place inside your customer’s browser, not on your server. Nothing is copied out of your database. No order records are downloaded. A few lines of JavaScript read the card number out of the checkout form as it is typed, send a copy to a server the attacker controls, and let the payment carry on to your real provider. The customer gets an order confirmation. So does the thief.
That is the whole of it, and it explains why the usual breach instincts fail here. Server logs show a clean transaction. Your application never handled anything it should not have. The evidence lives in a page that was rendered once, in a browser you do not own, and then thrown away. This piece is a Magecart attack explained from inside that browser tab: the scripts that load, the hooks they place on your inputs, the request that carries the data out, and the tricks that make a quick look at the source come back clean.
Method, not a single gang
The name is a label. RiskIQ and other researchers used it for a cluster of criminals stealing card data from Magento carts, hence the compound, and kept using it as the number of separate crews grew into the dozens. There is no single operator to profile, no one toolkit to fingerprint. Groups buy skimmer kits, rent exfiltration infrastructure and copy each other’s tradecraft, which is why two incidents six months apart can look nothing alike in code and identical in shape. The background is set out in more detail in this explainer on Magecart as a method rather than a group.
Every incident has the same three beats. Get code onto the page where card details are typed. Read the fields. Send the values somewhere else. Everything else is variation.
The distinction from a database breach matters practically, not just semantically. In a database breach you have artefacts: query logs, exported files, access records. In a skimming incident the theft happened client-side, so the questions change. What scripts did the payment page load yesterday? Who can change them? Did any of them change without a ticket? If you cannot answer those three questions from records you already hold, you have no visibility into this class of attack at all.
The three routes onto a checkout page
Skimmer code arrives one of three ways, and the route decides who can even see it.
Through a third-party supplier. Ticketmaster UK, 2018: malicious JavaScript reached the payment page through a chatbot supplier’s hosted script. Ticketmaster’s own servers were never touched. Nothing in the merchant’s codebase changed. A file on someone else’s CDN changed, and that file had permission to run on a page where card numbers were being typed. A chat widget has no business on that page.
Through modified first-party script. British Airways, 2018: an existing script on the airline’s own site was altered, and payment details were posted to a look-alike domain built to resemble the airline’s own infrastructure. British Airways’ own breach disclosure put the figure at roughly 380,000 affected transactions, and the ICO issued a £20m penalty in October 2020. One modified file. That is the cost side of this.
Through the platform itself. Magento 1 reached end of life in June 2020. In September 2020, security firm Sansec reported roughly 2,000 stores skimmed over a single weekend, which remains the clearest illustration of the platform route at scale. Nobody targeted those merchants individually. They were simply reachable.
Underneath all three sit the dull entry points: stolen admin credentials with no second factor, an unpatched extension, an integration user with write access that nobody has reviewed since the site launched. Magento-family platforms also allow JavaScript inside CMS blocks and widgets, which means injected code can live in the database rather than in any file your deployment pipeline touches. Grep the codebase and it comes back clean. Eight incidents grouped by entry route are laid out in this walkthrough of high-profile cases.
What the code does once it runs
A modern skimmer is patient. It does not read the DOM on page load, because on a single-page checkout the card fields do not exist yet. It registers a MutationObserver and waits for them to appear, then attaches listeners.
The hooks are ordinary web development, used for theft:
- submit on the payment form, which catches the complete set of values in one pass
- blur or change on each input, which catches values field by field and survives a customer who abandons and returns
- keyup, less common now because it is noisy, but it captures partial numbers even if the order is never placed
Reading values is trivial where the inputs belong to your page. Where card fields sit in a payment provider’s iframe, the skimmer cannot reach them, so it draws its own form on top: a convincing card block, styled from your own CSS, positioned over the real iframe. The customer types into the fake, the script harvests the values, then passes them into the genuine fields or simply reloads the step so the customer types them again into the real form and the payment goes through. Frustrating, but it works, and it works against merchants who believe an iframe ended the conversation.
The haul is never just the PAN. Expiry, CVV, cardholder name, billing address, email and phone all come from the same page, and all of them are useful for card-not-present fraud. Personal data is also in scope of UK GDPR, which changes who you have to tell.
Then the obfuscation. String concatenation, hex-escaped identifiers, base64 blobs passed to atob, character-code arrays, occasionally a packer. None of it is clever cryptography. Its only job is to survive a human reading the page source at speed, and it usually does, because a minified analytics bundle and a minified skimmer look much the same at a glance. Read for behaviour rather than beauty: any script that touches card input fields, and any script that builds a URL or a domain name at runtime out of pieces, deserves to be read in full.
How the data leaves the page
Exfiltration is where skimmers are easiest to catch, because something must leave the browser.
The classic pattern is an image request. The script creates an Image object, sets src to a URL on an attacker-controlled host with the stolen values base64-encoded into the query string, and the browser dutifully fetches it. No response is needed. Others use fetch or XMLHttpRequest to POST to a look-alike domain, navigator.sendBeacon, or a WebSocket for the more careful operators.
Hostnames are chosen to pass an eyeball check. Words like cdn, static, assets, js, metrics and pixel, combined with a plausible brand fragment, in a two-second scan of the network panel. The British Airways exfiltration domain was built on exactly that principle. Typosniffing variants add or drop a single character from a name you already trust.
The timing conditions are the part most write-ups miss, and they matter more than the obfuscation:
- The payload only executes when the URL contains a checkout path, commonly checkout, onepage, payment or success
- It fires once per session and sets a flag in localStorage or a cookie, so a repeat load shows nothing
- It aborts if developer tools appear to be open, detected by window dimension differences or by timing a debugger statement
- Some skip visitors from the merchant’s own country, or from addresses that have hit the admin panel
Which means one thing for your testing method. Opening the homepage, viewing source and finding nothing is not evidence of anything. Neither is opening the checkout with DevTools already docked. Test the real payment step, with a real basket, in a fresh profile, and compare the request waterfall against what that page is supposed to call. A clean payment page calls your own origin, your payment provider, and the small handful of suppliers you have consciously accepted. Nothing else. A POST or an image request to an unfamiliar host, carrying a long encoded string, fired on form submit rather than page load, is the shape you are looking for.
Where each defence breaks the chain
Three controls do real work here. Each one breaks a different beat, and each has a failure mode worth knowing before you deploy it on a live checkout.
Content Security Policy
CSP is the control that stops the data leaving. Get script-src right and unauthorised code will not load in the first place; get connect-src and img-src right and the beacon has nowhere to go. Note that pairing: teams tighten connect-src, congratulate themselves, and leave img-src wide open, which is precisely the channel the image-beacon skimmer uses.
Start in report-only mode. On a payment page there is no other sane option, because a mistyped directive in enforcing mode is an outage on your highest-value page. Collect reports for a full week, across card payments, wallet payments, 3-D Secure step-ups and the failure paths, then read them. They will be noisy, and the noise is informative: legacy checkout themes are stuffed with inline onclick handlers, and payment providers routinely load further domains at runtime that appear in no integration document. Prefer nonces over hashes for inline blocks you control, since hashes break the moment a template changes by one character. A worked directive set is given in this checkout CSP setup.
The honest limit: if an attacker exfiltrates through a domain your policy already allows, CSP does not see a problem.
Subresource Integrity
SRI pins a supplier’s file to a hash. If the file changes by one byte, the browser refuses to run it. That is exactly the Ticketmaster scenario stopped at the point of load.
It is powerful and brittle in the same breath. Hash-pinning suits versioned, immutable URLs. Pin a supplier’s rolling CDN endpoint and their next silent update stops the script dead, which on a payment page can mean a dead checkout rather than a blocked skimmer. Pin what is versioned. For anything that updates on the supplier’s schedule, the answer is usually to remove it from the payment page instead.
Hosted payment fields
The cheapest fix available. Moving the card inputs into your provider’s iframe takes the PAN and CVV out of your DOM at configuration level, so a skimmer on your page has nothing to read from the fields that matter. Most merchants can do this with a settings change and a template edit, which is why it belongs ahead of any monitoring purchase. The reasoning is set out in this note on hosted payment fields.
It is not immunity. The overlay attack described above still works, and name, billing address, email and phone still sit in your own form. Change detection on the containing page is still required.
Detecting a live skimmer, and what PCI DSS 4.0 now asks
The audit routine is unglamorous and takes about an hour:
- Load the real payment step in a clean browser profile with a real basket. Record every script the page requests, including those loaded by other scripts.
- Diff that list against a written manifest of what should be there.
- Read anything new in full. Not skim. Read.
- Repeat with DevTools closed, capturing traffic at the proxy or network level instead, to defeat the anti-debug checks.
- Check the database-held content too: CMS blocks, widgets, email templates, layout XML.
Be realistic about how these incidents usually surface. The first signal is often external: an acquirer calling about a common purchase point, an issuer’s fraud report, or a run of customers saying the only place they used that card was your site. By then the skimmer has been running for weeks. Detection signals and their reliability are covered in how to tell if your checkout has been skimmed.
Since 31 March 2025 this stopped being optional. PCI DSS 4.0 requirements 6.4.3 and 11.6.1 apply to every script loaded and executed in the consumer’s browser on the payment page. In plain terms, 6.4.3 asks you to authorise each script, keep an inventory with a written business justification for why it is there, and assure its integrity. 11.6.1 asks for a mechanism that detects and alerts on unauthorised change to the HTTP headers and page content of the payment page, run at least weekly. The requirement text is on the PCI Security Standards Council site, and the practical reading is in this breakdown of 6.4.3 and 11.6.1.
You do not need to buy a monitoring platform to satisfy both. A JSON manifest of authorised scripts in version control, with the justification and owner written next to each entry. SRI hashes on the versioned files. A CSP report endpoint that someone actually reads. A scheduled job, weekly at minimum, that renders the checkout headlessly, extracts the script list and response headers, and diffs them against the manifest, failing loudly on any difference. That is a day of engineering time and it produces the evidence an assessor asks for.
If you find injected code, resist the urge to delete it immediately. Preserve first: the exact file or database row, its modification timestamp, the full script content, admin and integration login records, deployment history, web server and CDN access logs for the exfiltration host, and a saved copy of the rendered page. Then remove the code, rotate every admin credential and API key, and invalidate sessions. Tell your acquirer, because the card brands may require a forensic investigation and that decision is not yours. Report to the ICO within 72 hours if personal data was affected. Cleaning up before capturing evidence destroys the only record of how long it ran and how many cards went out, and those are the two numbers everyone will ask you for.
The short version
A skimmer is a few lines of JavaScript with permission to run on your payment page. It waits for the card fields, reads them, and sends a copy to a host with a sensible-looking name. Take the card inputs out of your DOM with hosted fields, pin what you can with SRI, and use CSP in report-only mode for a week before you enforce script-src, connect-src and img-src. Keep a script manifest in version control and diff the rendered checkout against it weekly, which is also what 6.4.3 and 11.6.1 have required since March 2025. Nothing here is expensive. The expensive part is finding out from your acquirer.
Frequently Asked Questions
Is a Magecart attack the same thing as web skimming or formjacking?
Near enough. Web skimming and formjacking describe the technique of stealing data from a form in the browser; Magecart is the name researchers attached to that technique when used against ecommerce checkouts, and it stuck. Some vendors use e-skimming as well. The mechanism is identical whatever the label.
How can I tell if malicious JavaScript is running on my checkout right now?
Load the real payment step with a genuine basket in a fresh browser profile and list every script the page requests, then compare that list against the scripts you have authorised. Read anything unexpected in full, and watch for a request on form submit to a host you do not recognise carrying a long encoded string. Repeat the test with developer tools closed and capture traffic at the network level, because many skimmers abort when they detect an open console.
Does a Content Security Policy stop a Magecart attack on its own?
It stops most of them, and it is the single most effective header you can add to a payment page, but it is not absolute. A policy that permits a domain the attacker can use for exfiltration will let the data out, and a misconfigured policy in enforcing mode can break your checkout. Deploy it in report-only mode first, read a full week of reports across every payment path, then tighten.
Do hosted payment fields make my checkout skimmer-proof?
No. They remove the card number and CVV from your own DOM, which is a large and cheap reduction in risk, but an attacker can still overlay a fake card form above the provider’s iframe, and name, billing address, email and phone remain in your form. Change detection on the containing page is still needed.
What do PCI DSS 4.0 requirements 6.4.3 and 11.6.1 require me to do about scripts?
6.4.3 requires you to authorise every script that runs on the payment page, maintain an inventory with a written business justification for each one, and assure its integrity. 11.6.1 requires a mechanism that detects and alerts on unauthorised changes to the payment page’s content and HTTP headers, checked at least weekly. Both have been mandatory since 31 March 2025, and a version-controlled manifest, SRI hashes and a scheduled render comparison satisfy them without buying a product.
