---
title: Magecart and injected scripts: how to catch a tampered page
description: A skimmer doesn't break your checkout. It copies every card number typed into it and lets the payment go through, which is why the people who notice are usually a bank, a researcher or a stranger on social media. What happened at British Airways, Ticketmaster and polyfill.io, how the code gets in, what PCI DSS 4.0 now requires, and how to detect a tampered page yourself.
canonical: https://watchfor.io/blog/magecart-injected-scripts
---

# Magecart and injected scripts: how to catch a tampered page

2026-10-03 · WatchFor Team · Security · security, web, monitoring

A skimmer doesn't break your checkout. It copies every card number typed into it and lets the payment go through, which is why the people who notice are usually a bank, a researcher or a stranger on social media. What happened at British Airways, Ticketmaster and polyfill.io, how the code gets in, what PCI DSS 4.0 now requires, and how to detect a tampered page yourself.

The worst thing about a card skimmer on a website is how well it works. The
checkout loads, the payment goes through, the order confirmation arrives.
Somewhere in the page, a few extra lines of JavaScript have read the card
number, the expiry date and the security code as they were typed and sent a
copy to someone else. Nothing fails. Nothing is slow. Your monitoring is
green, because by every measure it has, the page is fine.

"Magecart" is the name the security industry uses for this: groups and
campaigns that inject skimming JavaScript into payment pages, also called
web skimming, e-skimming or formjacking. Below: three real incidents from
the regulators' and researchers' own documents, how the code gets in, what
stops it, what PCI DSS 4.0 requires, and how to detect a tampered page.

## Three incidents, from the primary sources

### British Airways, 2018: one edited JavaScript file

The UK Information Commissioner's
[penalty notice](https://web.archive.org/web/2022/https://ico.org.uk/media/action-weve-taken/mpns/2618421/ba-penalty-20201016.pdf)
lays it out. On 22 June 2018 an attacker got into an internal BA application
with compromised credentials for a Citrix remote-access gateway and moved
across the network. Eventually they edited a JavaScript file on
britishairways.com so that payment card data was copied to `BAways.com`, a
domain they controlled. The skimming code ran for 15 days, from 21 August to
5 September 2018. During that time, "when customers entered payment card
information into BA's website, a copy was sent to the Attacker, without
interrupting the normal BA booking and payment procedure."

BA found out on 5 September, when a third party told it that data was being
sent from britishairways.com to BAways.com. The ICO estimated that the
personal data of about 429,612 people was affected, including names,
addresses, card numbers and CVVs for 244,000 of them. Its notice of intent in
July 2019 proposed a £183.39 million penalty. The final fine, issued on
16 October 2020, was £20 million.

Two details are worth keeping. The script was BA's own, so a list of
approved third-party domains would not have flagged it. And the destination
was chosen to look like BA in a network log.

### Ticketmaster UK, 2018: a chatbot on the payment page

Ticketmaster used a customer-service chatbot from a company called Inbenta.
According to the ICO's
[penalty notice](https://web.archive.org/web/2022/https://ico.org.uk/media/action-weve-taken/2618609/ticketmaster-uk-limited-mpn.pdf),
the chatbot's JavaScript was hosted on Inbenta's server, and Ticketmaster
chose to include it on several pages, the payment page among them. An
attacker modified that JavaScript on Inbenta's side so that it "scraped"
what users typed: names, card numbers, expiry dates and CVVs.

The timeline is the uncomfortable part. On 6 April 2018 around 50 Monzo
customers reported fraud; by 19–20 April Monzo had replaced 6,000 cards. On
6 May, a user on Twitter posted that "Inbenta's website serves two different
files ... one of them has the infected line in it and the other one
doesn't." On 9 May, after a follow-up, Ticketmaster replied that "this is
not a virus, it's the help widget that is found on our home page." The user
answered that the file had "an extra line in it submitting information to a
website hosted by an External person in the UAE". The malicious code was identified on 23 June 2018. The ICO
notes that 9.4 million people in the EEA were notified as potentially
affected, and on 13 November 2020 it fined Ticketmaster £1.25 million.

The lessons differ from BA's. The script's address never changed, only its
contents. Different visitors could get different files. And the ICO put the
exposure down to "Ticketmaster's business decision to include the chat bot
on its payment page."

### polyfill.io, 2024: the domain changed owners

`polyfill.io` served a popular open-source library that patches missing
features in old browsers. Sites included it with one `<script>` tag pointing
at `cdn.polyfill.io`. According to
[Sansec's research](https://sansec.io/research/polyfill-supply-chain-attack),
published on 25 June 2024, a Chinese company bought the domain and the
GitHub account in February 2024, and the domain was then caught injecting
malware through every site that embedded it, more than 100,000 by Sansec's
count. The payload Sansec decoded redirected mobile users to a sports
betting site, and it was careful: it only activated on specific mobile
devices at specific hours, not when it detected an admin user, and it
delayed itself when it found web analytics on the page. Within days
Cloudflare was rewriting requests to its own copy and Namecheap had put the
domain on hold.

This one wasn't card theft, but the mechanism is a skimmer's, and it beats
two common defences. The code was generated per request from the visitor's
headers, so there was no single file to pin with a hash. And it hid from the
visitors most likely to look: desktops, admins, anyone outside the targeted
hours.

## How the code gets onto a page

Those three cases cover most of the routes in:

- **Your own server or build.** Stolen credentials, a vulnerable admin
  panel, a compromised CI job or a malicious dependency, and your own
  JavaScript gains a few lines. This is the BA pattern.
- **A third-party script you chose to load.** Chat, analytics, A/B testing,
  reviews, tag managers, consent banners. Each is a live code feed from
  someone else's server, and when they're compromised, so is every page that
  includes them. This is Ticketmaster.
- **A domain or package that changes hands.** An abandoned project, an
  expired domain, a maintainer account sold or taken over. The URL in your
  HTML is the same one you vetted years ago. This is polyfill.io.
- **CMS and shop platforms.** A plugin or theme update, a stolen admin
  password, or a marketer's "add custom script" field that puts code on
  every page without a deploy.

Once it's running, the skimmer needs to get the data out. The common ways
are a `fetch` or XHR request to a lookalike domain, a tracking pixel whose
URL carries the data, a WebSocket, a form whose `action` now points
somewhere else, or a fake payment form drawn over the real one.

## Defences that actually stop it

**A Content Security Policy.** CSP lists where scripts may load from
(`script-src`), where the page may send data (`connect-src`) and where forms
may submit (`form-action`). A skimmer loaded from a new domain is blocked,
and one that tries to post to `BAways.com` can't. Turn reporting on, and the
browsers of your real visitors tell you about every blocked attempt, which
is the closest thing to a skimming alarm that runs on every page view. It
has limits: it can't help when an allowed domain is the one compromised (the
Inbenta case), data can be smuggled out through an allowed analytics domain,
and `'unsafe-inline'` in `script-src` undoes much of it. Start in report-only
mode; [Content Security Policy explained](/blog/content-security-policy-explained)
and our [security headers checklist](/blog/security-headers-checklist) cover
the rollout.

**Subresource Integrity (SRI).** An `integrity` attribute pins a script to a
hash. If the file changes by one byte, the browser refuses to run it
([MDN](https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity)):

```html
<script
  src="https://cdn.example.com/lib@4.2.1/lib.min.js"
  integrity="sha384-M203vLCvZdI2DAFEpVs3YNPxNFNtwrWrHY9XrXkEzOjLmndNNAWAdMk0uRYc2i2r"
  crossorigin="anonymous"></script>
```

Generate the hash from the exact file you reviewed:

```bash
openssl dgst -sha384 -binary lib.min.js | openssl base64 -A
```

SRI is perfect for versioned files that never change, and it does nothing for
scripts that are supposed to change: a vendor's "always latest" snippet, a
tag manager, or a polyfill service that generates code per browser. For
those, the honest options are to self-host a reviewed copy or to accept that
you're trusting the vendor and watch what it serves. The new
`Integrity-Policy` header can make the browser refuse any script without an
`integrity` attribute, and its report-only variant tells you which ones
still lack one.

**A script inventory, and fewer scripts on payment pages.** Write down every
script that runs on checkout, who owns it and why it's needed there. Then
remove the ones that aren't. A chatbot, a heatmap tool or a marketing pixel
on the page where card numbers are typed is a risk you're taking for very
little. If you can, use your payment provider's hosted fields or hosted
payment page, so card data is typed into their frame rather than your DOM.
That shrinks the problem but doesn't remove it: a script on your page can
still draw a fake form over theirs.

## What PCI DSS 4.0 requires: 6.4.3 and 11.6.1

PCI DSS v4.0 added two requirements aimed squarely at this. Both were best
practice until 31 March 2025 and are mandatory since. In v4.0, as printed in
the
[SAQ A-EP](https://listings.pcisecuritystandards.org/documents/PCI-DSS-v4-0-SAQ-A-EP.pdf):

> **6.4.3** All payment page scripts that are loaded and executed in the
> consumer's browser are managed as follows:
> - A method is implemented to confirm that each script is authorized.
> - A method is implemented to assure the integrity of each script.
> - An inventory of all scripts is maintained with written justification as
>   to why each is necessary.

> **11.6.1** A change- and tamper-detection mechanism is deployed as follows:
> - To alert personnel to unauthorized modification (including indicators of
>   compromise, changes, additions, and deletions) to the HTTP headers and
>   the contents of payment pages as received by the consumer browser.
> - The mechanism is configured to evaluate the received HTTP header and
>   payment page.
> - The mechanism functions are performed as follows:
>   - At least once every seven days OR
>   - Periodically (at the frequency defined in the entity's targeted risk
>     analysis, which is performed according to all elements specified in
>     Requirement 12.3.1).

The applicability notes say 6.4.3 covers scripts from your own environment
and from third and fourth parties, and that 11.6.1 is not about installing
software on your customers' devices but about techniques that detect
unexpected script activity. PCI DSS v4.0.1 narrowed 11.6.1 to "security-impacting
HTTP headers and the script contents of payment pages", as summarised in
the PCI SSC's own
[community meeting session](https://www.pcisecuritystandards.org/wp-content/uploads/2024/09/Day-1_Track-2_7_JMan_JZitomer_FINAL.pdf).

Two later changes are worth knowing. In January 2025 the Council
[removed 6.4.3 and 11.6.1 from SAQ A](https://blog.pcisecuritystandards.org/important-updates-announced-for-merchants-validating-to-self-assessment-questionnaire-a)
(merchants who fully outsource payment) and replaced them with an
eligibility criterion: the merchant must confirm "their site is not
susceptible to attacks from scripts that could affect the merchant's
e-commerce system(s)". The
[FAQ on that criterion](https://pcisecuritystandards.org/faq/articles/Frequently_Asked_Question/how-does-an-e-commerce-merchant-meet-the-saq-a-eligibility-criteria-for-scripts)
says it applies when your page embeds the processor's payment form in an
iframe, and that you can meet it with techniques like those in 6.4.3 and
11.6.1 or with confirmation from your processor. And in March 2025 the
Council published an information supplement, *Payment Page Security and
Preventing E-Skimming*, with implementation guidance for both requirements.

The Council is clear that it doesn't decide whether a given implementation
is compliant; your acquirer or assessor does. Treat everything below as
tooling that can support those requirements, not as a certificate.

## Detection: what a useful check has to see

CSP reports come from real visitors and are the best signal you have, but
they only cover what your policy forbids. To catch a change to something
the policy allows, such as an allowed vendor serving new code or a form
quietly rewired, you need something that looks at the page itself. Going by
the three cases above, that something has to:

1. **Compare with a baseline you approved, not with the last check.** If
   every check is compared with the previous one, a skimmer that survives
   one check becomes "normal". Ticketmaster's code was live for months.
2. **Render the page in a real browser.** Plenty of scripts are added by
   other scripts, and a tag manager can load a new vendor without your HTML
   changing at all. Reading the HTML misses all of it.
3. **Compare script contents, not just script addresses.** Inbenta's URL
   never changed. BA's file kept its name.
4. **Watch where data goes.** A new form target, a new `fetch` destination,
   a new beacon host. `BAways.com` would have been a new destination on the
   first page load after the edit.
5. **Keep looking from more than one place.** A file served to some
   visitors and not others, as Inbenta's apparently was, can look clean on
   any single check.

And it has to accept your own deploys without crying wolf, or the team
will mute it within a week.

Be honest about the limits too. Malware that targets particular devices,
hours or regions, or hides from admins, as the polyfill.io payload did, can
evade any scheduled check some of the time. That's why the detection side
works best next to CSP reporting, not instead of it.

## Catching it with a page integrity monitor

This is what WatchFor's [page integrity monitor](/docs/monitors/integrity)
was built for. You give it the pages an attacker would pick (home, login,
checkout) and the first check records a **baseline** for each one: the
scripts it loads and a fingerprint of each script's code, its forms and
where they submit, iframes, outbound link domains, the security headers and
the visible text. Every later check is compared with that baseline until
someone accepts a change. Each kind of finding is its own alert rule:

![Alert rules of a WatchFor page integrity monitor: Unknown script source, Form sends data elsewhere, Tamper keywords, Redirects elsewhere, Security header weakened, Third-party script changed, Page content changed, Security headers changed and First-party script changed, each switched on](/blog/magecart-injected-scripts-rules.webp)

Mapped onto the incidents above:

- **Unknown script source** (critical): a page loads JavaScript from a host
  that isn't your site, isn't in the baseline and isn't trusted.
- **Third-party script changed** (critical): a vendor script keeps its
  address but serves different code. That is the Inbenta case exactly.
- **First-party script changed** (warning): one of your own scripts changed.
  Usually that's your deploy; once, at BA, it wasn't. It's a warning rather
  than an alarm because it happens on every release.
- **Form sends data elsewhere** (critical) and, in browser mode, **New data
  destination** (warning): the page now submits or sends requests to a new
  host, through fetch, XHR, beacons, WebSockets or tracking pixels.
- **Security header weakened** (critical): the CSP that was blocking the
  skimmer got removed or loosened.

**Browser mode** renders every page in Chrome and compares everything it
actually loaded, scripts added at runtime included, and photographs the page
so the baseline and the current state can be compared side by side. Here
is a real example from a demo organization, watching three pages of Hacker
News in browser mode. The front page of a news site changes all day, so the
findings are new external links and changed text rather than anything
malicious, but it shows how a finding reads:

![Findings on a page integrity monitor watching three Hacker News pages: a new external link on the front page, three new external links on /newest, and the text on both pages changed by 65% and 60% against the baseline, with a per-page table of text change, status and when the baseline was recorded](/blog/magecart-injected-scripts-findings.webp)

Open a page and it shows exactly what differs: the word diff, each script
marked **NEW** or **CODE CHANGED**, forms, data destinations, headers, and
the baseline and current screenshots:

![A watched page opened in WatchFor: the text changed 65% against the baseline, security headers unchanged, one script source, one known form target, baseline and current screenshots side by side, the new external link listed as a finding, and an Accept as new baseline button](/blog/magecart-injected-scripts-page.webp)

A news front page is the noisiest thing you could watch, which is why each
page can have its own sensitivity, and why text changes are warnings while
scripts, forms, redirects and weakened headers are critical. On a checkout,
the text barely moves and a new script is never routine.

**Your own deploys don't have to alert.** As the last step of your release
pipeline, one request checks the pages and accepts what that check saw as
the new baseline:

```bash
curl -s -X POST "https://watchfor.io/api/v1/monitors/<monitor-id>/baseline?fresh=true" \
  -H "Authorization: Bearer wf_live_YOUR_WRITE_KEY"
```

The [page integrity docs](/docs/monitors/integrity#accept-changes-after-a-deploy)
explain what happens when the check takes longer.

For a vendor you know, **Trust this source** stops its domain being
reported as new on any page of the monitor. Use it sparingly: a trusted
source's code is no longer compared either, and Inbenta was a vendor
Ticketmaster trusted. It's the right call for your payment provider's own
script, less so for a chat widget. And you can let unreviewed changes become the baseline
automatically after a few hours, but security findings (new scripts,
changed code, forms, headers, redirects) always wait for a person unless you
explicitly opt them in too. An attacker only needs to wait that long.

A security incident only closes after **three clean checks in a row**, so
one clean check from another location can't close it for a script served to
some visitors only. And these incidents are **never published to a status
page automatically**: tampering isn't downtime, and the details could show
attacker hosts to the public.

Page integrity is on the Starter plan and above, with browser mode from Pro.
Checks run as often as every five minutes on Pro, far inside the PCI
"once every seven days" floor. The [docs](/docs/monitors/integrity) list
every finding, and the [page integrity overview](/page-integrity-monitoring)
shows the rest.

## A checklist for your checkout this week

1. **List every script on your payment pages.** Open DevTools, reload the
   checkout, filter the Network tab by JS. Write down the owner and the
   reason for each one. Remove what has no reason.
2. **Ship a CSP in report-only mode** with `script-src`, `connect-src` and
   `form-action` restricted, and a reporting endpoint. Read the reports for
   a week, then enforce.
3. **Add SRI to every static third-party file** you can pin. Self-host the
   ones that never need to change.
4. **Prefer hosted payment fields** from your payment provider, so card
   numbers are typed into their frame.
5. **Watch the pages from the outside**, against a baseline you approved,
   in a real browser, and make your deploy pipeline accept its own changes.
6. **Decide who gets the alert**, and what they do in the first hour: pull
   the script, block the destination, tell your payment provider.

In every case above, the alarm came from outside: a third party told BA,
banks and a stranger on Twitter flagged Ticketmaster, and most site owners
learned about polyfill.io from a security firm's blog post. The point of all
this is that next time, the first to know is you.

---

Canonical page: https://watchfor.io/blog/magecart-injected-scripts · All articles: https://watchfor.io/blog/all.md · Site guide: https://watchfor.io/llms.txt
