Magecart and formjacking, how to detect a client-side skimmer
CentralCSP Team ·
Last update:
Magecart is the name for a family of attacks that steal payment-card and form data by injecting malicious JavaScript into a checkout page. The technique is called formjacking: a skimmer reads what a customer types into the payment form and quietly sends a copy to the attacker, while the legitimate transaction still goes through. Because the skimmer runs in the browser, in code that loads the same way your real scripts do, your servers and your web application firewall never see it. The way to catch it is on the client side: inventory the scripts that actually run, alert when that set changes, and constrain what an injection can do with a strict Content Security Policy (CSP).
This is the threat companion to how attackers abuse Google Tag Manager, which covers one common delivery path, and to CSP for PCI DSS v4, which covers the compliance requirements built around exactly this risk.
What Magecart and formjacking are
A formjacking skimmer is a small piece of JavaScript that attaches to the payment form and captures field values: card number, expiry, CVV, name, address. It usually waits for a submit or a blur event, reads the inputs, and exfiltrates them to an attacker-controlled host, often disguised as an analytics or image request. The customer sees nothing. The order completes, the receipt arrives, and the card data has already left.
Magecart is the umbrella term for the threat groups and toolkits that run these attacks at scale. The name comes from early campaigns against Magento stores, but the technique is platform-agnostic. Any site with a payment or login form is a target, and the skimmer code is often heavily obfuscated and changes frequently to evade signature matching.
How the skimmer gets onto the page
The injection rarely comes through your own code. It comes through something your page already trusts.
- A compromised third-party script. A chat widget, analytics tag, A/B testing snippet, or payment SDK is loaded from a vendor. If that vendor is breached, or their CDN is, the file your customers fetch changes, and now it skims. You shipped nothing; the script you already include did the work.
- A tampered CDN or dependency. A script you host on a CDN, or a package in your build, is altered upstream. The URL is the same, the integrity is not.
- A hijacked tag manager. A tool whose entire job is injecting scripts is an ideal channel. A stolen account or an over-permissioned editor can push a skimmer to every page without touching your repo, the scenario covered in the Google Tag Manager security risk.
- A server-side or supply-chain compromise that edits the page template directly. Less common for the third-party-heavy cases, but it does happen.
In every path, the malicious code arrives the same way legitimate code does, which is the whole problem for detection.
Why your server never sees it
A web application firewall inspects requests and responses at your origin. A skimmer injected through a third-party script does not pass through your origin at all: the browser fetches it directly from the vendor or CDN, runs it, and the stolen data goes straight from the browser to the attacker's host. None of that traffic touches your servers.
Source-code scanning has the same blind spot. Your repository and your build pipeline look clean, because the change happened in a dependency you load at runtime, not in code you committed. The page as the customer's browser assembles it is different from the page your CI built. That gap is where formjacking lives, and it is why both PCI DSS v4 requirements that target this threat ask for evidence from the browser, not the server.
Detect it from the client side
If the only place the skimmer is visible is the browser, that is where the detection has to run. Three controls work together: a baseline of what runs, an alert when that baseline changes, and a policy that limits what a stray injection can do.
Inventory every script that runs
A script inventory is a structured list of every script the browser actually loads and executes on a page, first-party and third-party, built from CSP hash reporting. CSP Level 3 lets the browser compute a hash of each script it fetches and report it, so you get the real list from real page loads, not an estimate from your build. With the technology, version, and known CVEs attached, the inventory is your known-good baseline: this is the set of scripts that belong on the checkout page.
Alert when the set of scripts changes
A baseline is only useful if you are told when it moves. Change alerting fires when the scripts on a sensitive page differ from the approved set: a new origin, a new inline block, a script that was not there yesterday, or the same URL serving a different hash. On a payment page, a new or changed script is the earliest signal of a skimmer, the difference between catching it in hours and finding out from a charge-back report weeks later. This is the change-and-tamper detection that PCI DSS v4 requirement 11.6.1 asks payment pages to run.
Constrain the injection with a strict CSP
Detection tells you something changed; a strict CSP limits what that change can do in the first place. Rather than trusting a list of hosts, a strict policy trusts a per-request nonce and uses 'strict-dynamic' to extend that trust only to scripts your trusted code loads. An attacker who finds an injection point cannot run a script, because they cannot guess the nonce, and a host allowlist they might abuse is ignored.
Content-Security-Policy:
script-src 'self' 'nonce-{SERVER-GENERATED-NONCE}' 'strict-dynamic';
connect-src 'self' https://api.your-psp.example;
object-src 'none';
base-uri 'none';
report-to csp-endpointReporting-Endpoints: csp-endpoint="https://<Endpoint-ID>.report.centralcsp.com"Two directives do extra work against formjacking. connect-src limits where the page may send data, so a skimmer trying to exfiltrate to an unknown host triggers a violation instead of a silent POST. base-uri 'none' blocks a <base> tag injection that could redirect every relative script URL to an attacker. Host allowlists are also where bypasses hide: an open redirect or a JSONP endpoint on an allowed host can run attacker-chosen code while still satisfying a loose policy, which is another reason to prefer a nonce over a host list. Build the policy with the Report-Only-first approach in how to build a strong CSP, and check a draft with the free CSP scanner.
How CentralCSP fits
CentralCSP ingests your CSP violation and hash reports, builds the script inventory for every page, and alerts when the set of scripts or the response headers change. A skimmer shipped through a compromised vendor or a rogue tag surfaces as a "new script on the checkout page" alert, with the origin and hash, rather than as a customer complaint later. Pair that with the CSP suite to enforce a strict policy, and you have both halves: limit what an injection can do, and know the moment one appears.
You can evaluate your current policy for weak spots like broad host allowlists, then start a free trial and point a Report-Only header from your checkout pages at it to see what those pages actually load before you enforce.
Frequently asked questions
What is Magecart?
Magecart is the name for the threat groups and toolkits that steal payment-card data by injecting malicious JavaScript (a skimmer) into checkout pages. The technique of reading form fields and exfiltrating them is called formjacking. The code usually arrives through a compromised third-party script, CDN, or tag manager.
How do you detect a formjacking skimmer?
From the browser side, because that is the only place the skimmer is visible. Build a script inventory of every script that runs on the payment page, alert when that set changes (a new origin, a new inline block, a changed hash), and run a strict CSP so an injection cannot execute or exfiltrate quietly. Server logs and source scans miss it.
Why does a web application firewall miss Magecart?
A firewall inspects traffic at your origin. A skimmer loaded through a third-party script runs in the browser and sends stolen data straight from the browser to the attacker, never passing through your servers. The page the customer's browser assembles differs from the one your build produced, and that gap is where the skimmer lives.
Does a CSP stop Magecart?
A strict CSP that trusts a nonce and 'strict-dynamic' rather than host allowlists stops an unrelated injection from running a script, and connect-src limits where data can be sent. It does not stop a skimmer shipped through a script you deliberately trust, which is why detection (inventory plus change alerting) is the other half.
The takeaway
Magecart and formjacking steal card data with JavaScript that loads exactly like your real scripts, so the server never sees it. Detection has to come from the browser: a script inventory as your baseline, change alerting when a new or modified script appears on a payment page, and a strict CSP that keeps an injection from running or exfiltrating in the first place. Those controls turn an invisible client-side change into an alert you can act on.
Further reading: OWASP on client-side security and security-vendor research on web skimming such as Akamai's analysis of Magecart campaigns.
Sources
- Akamai, Magecart web-skimming research
- OWASP, Top 10 web application security risks
- W3C, Content Security Policy Level 3