Cartwright - an LLM-generated "man-in-the-cart" payment-hijack kit

Cartwright - an LLM-generated "man-in-the-cart" payment-hijack kit

The checkout had stopped converting, and nobody could say why. Customers reached the cart, a tidy little "payment" panel slid into view, and then the orders simply evaporated. The owner of this twenty-year-old Italian e-commerce assumed a plugin had broken; his developer ran git status, saw a spotless source tree, and swore on it that nothing had changed. They were both right and both wrong. Nothing in the code had changed. The shop had been turned into a money funnel for a stranger, and the machinery doing it was, as far as I can tell, the most professional piece of criminal engineering I have taken apart in years, for the simple reason that most of it was not written by a criminal at all. It was written by a language model.

I spent the days that followed reverse-engineering the entire cascade, then published the artifacts, the indicators of compromise and the detection rules as an open incident-response repository on my GitHub account, ir-2026-04-cartwright, the campaign first observed live on 7 April 2026, so that other shops and CERTs can recognize it in their own logs. This article is the readable companion to that repo. I am writing it because the shape of this thing matters far beyond one store. For two decades I have cleaned up compromised PHP sites, and the playbook was always reassuringly familiar: a known skimmer, a recycled web shell, a string you could grep for and a signature someone had already named. This was none of that. The attack code was tailored to that one specific store, it understood the store's own checkout better than its owner did, and the kit cheerfully documented, in its own source comments, that the per-victim code is produced by an LLM. That is the shift worth your attention.

What does a "man-in-the-cart" attack actually do?

It hijacks the payment step and reroutes the money, instead of stealing the card. Classic Magecart skimming copies the card number as the victim types it; this does something colder and, frankly, smarter. It hides the shop's legitimate payment methods, paints over them with an iframe that impersonates the store's own payment area, and instructs the customer to pay by SEPA bank transfer, or to scan a QR with their banking app, for the exact live cart total. The buyer is certain they are paying the shop. They are paying a stranger, and the money is unrecoverable the instant the transfer clears. No card data ever changes hands, which is precisely why every card-centric fraud control in the stack stays fast asleep.

On the store I investigated, the carrier was a single stored <script> tag, rendered into the checkout page without escaping. Not one file on disk was touched, which is exactly why that git status came back clean and the developer's conscience came back clear. The payload lived in the data, not in the code, and that is the first hard lesson of this case, the one I want tattooed on every merchant's monitor: a clean source tree is not evidence that you have not been breached. Your repository can be pristine while your database quietly serves someone else's JavaScript to every shopper who reaches the till.

The injected tag pulled a small shared runtime and then performed a routine I had genuinely never seen automated before. It read the page, pre-selected one of the shop's seven legitimate payment radios so the store's own legacy shipping-total function would compute the correct amount, then dropped a stylesheet that hid the entire real payment block and the checkout button. In their place it slotted an iframe pointing at the attacker's gateway, the order total passed cleanly in the URL, and it kept a MutationObserver patrolling the DOM to re-inject itself the instant anything tried to remove it. To the customer it looked like a slightly redesigned payment section. It was a stranger's storefront wearing the shop's skin, stitched on so well that the seam was invisible.

The cascade, stage by stage

The operation is built like real software, because in every meaningful sense it is. There is a shared runtime the attacker reuses across victims, and a thin per-target layer that adapts it to each store. The shared runtime announces itself in its file header as a versioned library for "code-generated payment integrations", and the very next line is the one that gives the whole game away:

StagerRuntime - Runtime library for code-generated payment integrations
Version: 4.0.0
No declarative config needed - the LLM generates JavaScript that
orchestrates these helpers.

That StagerRuntime v4.0.0 exposes generic primitives the way any well-factored SDK would: inject an iframe, hide elements, parse a currency string in any European format, route postMessage events between frames, cache against double-payment. It even ships a hardcoded blocklist for Klarna's checkout objects, which tells you the authors are not aiming at artisanal carts; they are building to subvert mainstream payment widgets at scale. On top of that runtime sits the per-victim loader, served from a path dressed up as something boring and trustworthy: /libs/<SHOP_ID>/jquery-ui-core.js. A defender skimming network requests sees "jQuery UI" and moves on. It is not jQuery UI. It is the bespoke fraud loader for that one shop, and the tailoring is unmistakable: it knew the store's real element ids, it knew the name of the legacy shipping-calculation function to trigger, and its comments reasoned, in plain confident English, about why this particular shop was "a unique case" and how best to fit the fraud into its specific DOM.

The destination iframe, served from malumpay.click/pay/<ORDER_ID>?amount=<total>, is a fake SEPA gateway: a Node.js and Express application sitting behind Cloudflare, localized into fifteen European languages, with tabs for QR "scan and pay", digital banks (Revolut, N26, Wise, Monzo) and plain manual bank details. And then comes the detail that promotes this from a clever script to a genuine criminal enterprise: a live support chat runs inside that iframe, wired to /api/chat/{poll,send,history} and backed by a human operator, armed with pre-written answers to the exact objections a defrauded buyer raises. "The QR will not scan." "I already paid, what now?" Generative code for the attack, a staffed multilingual help desk for the close. If you have ever wondered what "fraud as a product" actually looks like in 2026, this is it, end to end.

If you run an online store on an aging stack and you cannot say with confidence whether your checkout has been quietly re-skinned like this, that uncertainty is exactly the gap I close. In my hub on AI for business security I collect the methodology I use for precisely this class of work, on real production systems rather than conference demos.

Was this really written by an LLM, or am I pattern-matching?

It was, with high confidence for the offensive code, and the evidence is layered rather than a single convenient tell. First, the kit declares it itself: the runtime comment quoted above states outright that an LLM generates the orchestration JavaScript, and the architecture, shared primitives plus per-target synthesis, is exactly what you would design if a model were writing the glue. Second, a separate file references "Cursor.com-style icons", which plants the developers squarely inside Cursor, the AI-native code editor whose flagship model is Claude. Third, the stylometry, and this is where it gets almost funny: the comments explain the why before the what, they narrate past bugs ("a previous version appended to an off-tree wrapper, which caused..."), they sprinkle defensive asides like "belt-and-suspenders" and "swallow the error", and they reach, again and again, for the em-dash, that typographic fingerprint frontier models cannot seem to resist. That is not how a human criminal comments throwaway malware at three in the morning. It is how a careful, slightly over-eager model writes when you ask it for production-grade code.

I want to be exact about confidence, because overclaiming is how threat intelligence quietly bankrupts its own credibility. The loader is, to my eye, provably LLM-generated. The break-in that planted it is a separate question I hold at medium confidence, and I will get to it. I have spent the last two years building production LLM automation, north of fifty codebases under agentic orchestration, so I read this code the way I read the output of my own pipelines, and the fingerprints were familiar to the point of discomfort. The symmetry was not lost on me either: I sat there using one frontier model to dissect malware that another, in all probability, had written. Same tool, opposite ends of the barrel.

The novelty here is not a new exploit. It is the economics. A shared runtime plus a model that writes a bespoke, DOM-aware loader per victim means the marginal cost of a tailored attack collapses toward zero, across fifteen languages, with no reused signature left behind to catch it.

This is the same coin I keep flipping from the defensive side when I audit AI-generated code for recurring vulnerabilities: the model is an amplifier, and an amplifier is supremely indifferent to which way you point it. The honest, non-luddite reading is that generative AI did not invent payment fraud; it industrialized the boring, expensive, time-consuming part of it, the per-target adaptation, and that is more than enough to bend the threat model out of shape.

The other half: an AI-orchestrated break-in

That <script> tag did not materialize out of goodwill; somebody had to write it into the store's database first, and the way they did it carries the same low-effort-per-target smell. Across several months of early 2026 the storefront had been hammered by an automated, error-based SQL injection campaign against an unauthenticated parameter, tens of thousands of requests from a carousel of rotating source addresses. The application committed both halves of the classic sin: it concatenated user input straight into SQL, and it echoed database errors back to the page, which is the textbook recipe for error-based exfiltration. The campaign dumped the user and admin tables, where passwords sat as unsalted MD5, the kind of thing a commodity GPU shreds offline before the coffee is cold. The admin login form was injectable too. With a valid session, and session handling that bound to no context and never bothered to expire, the attacker wrote a single poisoned value into a text field the checkout renders unescaped. That is the entire intrusion: no malware on disk, no modified file, one stored string that loads someone else's code on demand.

Was that half AI-driven as well? The individual payloads are standard sqlmap-grade tooling, so I do not claim a model pulled the trigger. What is entirely consistent with an LLM in the loop is the end-to-end orchestration: profile a tired legacy shop, locate the injectable parameter, exfiltrate and crack the credentials, log into the admin, map the checkout DOM, then emit a loader tuned to it. That "analyze the target, synthesize the best approach, move to the next" loop is exactly what a model makes cheap, and it is the same pressure I described when writing about protecting the software development flow from injected dependencies. The takeaway is not paranoia. It is that the attacker's per-target labor has been automated away, so your defenses should assume bespoke, not reused, and stop waiting for a signature that will never come twice.

Indicators of compromise

The whole point of publishing this is correlation: if you see any of the following in your own logs, DOM or database, you are very likely looking at the same campaign. These are the public indicators from the repository; the full set, with YARA rules (iocs/stager-cartwright.yar), iocs/indicators.csv and the redacted samples, lives there.

TypeIndicator
Loader domain (Cloudflare)trustaccept.click
Fake-gateway domainmalumpay.click
Third domaintheraw.events
Loader path (fake jQuery UI)/libs/<SHOP_ID>/jquery-ui-core.js
Runtime / gateway paths/stager/core.js, /pay/<ORDER_ID>, /js/chat-widget.js, /api/chat/{poll,send,history}
DOM / DB signaturesStagerRuntime, stager-runtime: ^4.0.0, data-stager-iframe, type:'sepa-payment'
stager/core.js SHA-256bc4447f60301d10ea9ad85951643df6139caac5bce517554110c932568f74248

All three domains were registered through the same registrar and fronted by a single Cloudflare account, sharing the same nameserver pair. And here is an operational warning that saves you from shooting your own customers in the foot: do not block the Cloudflare anycast ranges (172.67.*, 104.21.*, 188.114.*) at your firewall. They front a sizable slice of the legitimate internet, possibly including your own assets. Block the domains, kill the DNS resolution, and let a strict outbound policy do the rest. Threat intel is only useful if it does not break the patient while treating the disease.

Why this should worry any shop owner on an old stack

Because every weakness this kit leaned on is the default state of a neglected e-commerce, not some exotic zero-day. Unparameterized SQL, MD5 password storage, database errors leaking to users, sessions with no binding and no expiry, and DB-sourced text printed into HTML without escaping: that is the standard anatomy of a PHP storefront that has run for fifteen years on the doctrine of "if it works, do not touch it". None of it requires a sophisticated adversary anymore, and that is the entire point, because the sophistication has moved out of the human and into the tooling. The bar to weaponize a tired old shop has dropped through the floor, and the floor is where most legacy carts live.

The good news, and there is real good news, is that the defenses are equally unglamorous and they genuinely work. The order below is the one I gave the owner, and the same one shipped as detection/csp-and-hardening.md in the repo:

  • A correct Content-Security-Policy with a frame-src allowlist blocks the fraudulent iframe even if the loader runs, because the attacker's domain is simply not on your list. Start it in Content-Security-Policy-Report-Only mode, and the first report pointing at an unknown domain becomes your earliest, cheapest breach alarm. The reference to start from is OWASP on Content Security Policy.
  • Escape every database-sourced value on output with htmlspecialchars($value, ENT_QUOTES, 'UTF-8'). This neutralizes the stored-injection carrier at the sink, where it actually fires.
  • Parameterize all SQL with prepared statements and stop echoing database errors to users. That closes the error-based door the whole intrusion walked through.
  • Migrate passwords off MD5 onto a modern hash (password_hash() with bcrypt or Argon2id), bind admin sessions to context and expire them, and pin third-party scripts with subresource integrity so a swapped jquery-ui-core.js fails closed.

The repository also ships a plain-language merchant self-check (detection/how-to-check.md): the handful of things a shop owner, or their developer, can grep for in the database and the rendered page to know within minutes whether they are carrying this passenger.

I do not name the client, and I never publish identifiable case studies. The anonymity is the point, not a limitation: you verify my competence from the open-source artifacts, the IOCs and the methodology I publish, not from a logo on a slide. That is how I want trust to work in security, and it is the same standard I hold the attackers' "proof of payment" screen to.

There is a forward-looking edge to this as well. As checkout drifts toward agent-mediated and programmatic payment flows, the trust boundary between "the shop's page" and "the payment surface" gets harder for a human to verify by eye, the same tension I unpacked comparing the emerging agentic-commerce payment protocols for Italian B2B and B2C. A kit that re-skins a payment area this convincingly today is a preview of how thin that boundary becomes when the "customer" is itself partly automated, and nobody is looking at the pixels.

A final word on disclosure, because doing this responsibly is part of doing it at all. The three domains behind the kit are being reported to the CDN, to the browser safe-browsing programs, to the national CERT and to law enforcement, with the public indicators packaged so other victims can correlate the campaign in their own telemetry. On the store itself: the injected value was removed from the database, the SQL injection and the output-escaping holes were closed, credentials and sessions were rotated, and only then was the checkout brought back online. Cleanup without closing the entry point is theater. The tag just gets re-injected, and you have learned nothing except how to repeat the same bad night.

What stays with me from this engagement is not the cleverness of the fraud, which was real and considerable, but how astonishingly cheap that cleverness has become. A criminal no longer needs to understand your specific shop; a model understands it for them, writes the integration, and a call-center closes the sale in the buyer's own language. That asymmetry is the actual story, and it is why I keep insisting, on both sides of the fence, that adopting AI is an engineering discipline and not a parlor trick. The shops that come through this era intact will be the ones that treated their old, quiet, "working" systems as the attack surface they have always been, and brought someone in to look before the checkout stopped converting, not after. If you run a store on a codebase you no longer fully understand, and you would rather find out where it is exposed on your own terms than on an attacker's, the fastest way to start is my free assessment form for AI and security work: a handful of questions, two minutes, and an honest answer on whether it is something I can help with.

Ultima modifica: