The message came through Upwork. A company that sells solar panels, inverters, and off-grid power systems had a problem that was equal parts alarming and embarrassing: their own store was sending shoppers to random websites, and some visitors were getting new browser tabs full of fake “verify you are human” CAPTCHA pages. For a store that asks customers to trust it with thousands of dollars on a single pallet of panels, “my website occasionally throws you at a scam page” is not a small thing. It erodes exactly the trust the business runs on.
The hard part wasn’t the symptom but rather that nobody could pin it down. It happened only sometimes to some people but not and the owner would try to reproduce it, the site would behave perfectly.
That gap, real victims on one side and a clean bill of health on the other is usually the most important clue in an investigation. A problem that hides from the people looking for it isn’t behaving randomly but is rather being selective, and someone builds that selectivity on purpose.
- platform
- Shopify Plus storefront
- symptom
- Intermittent redirects to random sites and fake CAPTCHA pages
- business risk
- Shoppers were being pushed away from a high-ticket solar equipment store
- scanner result
- Clean, because the script stayed silent for crawlers and automated browsers
- root cause
- Orphaned app snippet still loading a hostile remote script after the app was uninstalled
A Redirect Nobody Could Reproduce
I started where I always start: from the outside, as a stranger. Before touching anything in the store’s admin, I wanted to see what a real visitor’s browser actually receives.
The first useful fact came in the response headers. This was a Shopify Plus site and that matters because it rules out whole categories of infection. Shopify hosts the platform so an attacker can’t drop a PHP backdoor in a core file or poison a database the way they can on self-hosted WordPress. On Shopify, malicious front-end behavior almost always comes from one of a much smaller set of places: injected code in the theme, a compromised third-party app, a Google Tag Manager container, or a custom pixel. The list is short, which works in our favor.
I pulled the homepage as a desktop browser, then as an iPhone, then as an Android device, then as someone arriving from a Google search. If the site was serving a different, poisoned version of itself to certain visitors, I’d expect the HTML to change.
It didn’t though…every version came back byte-for-byte identical but…that told me something useful: Shopify’s servers weren’t injecting the malicious behavior based on who was asking. Whatever was redirecting people was riding along in the same code everyone received, then deciding what to do later inside the visitor’s own browser.
The trap was in the page, it just wasn’t springing for me yet.
The First Wrong Turn: Google Tag Manager
The page loaded a Google Tag Manager container. On a store like this with Google Ads, a Meta pixel, Microsoft advertising, and analytics everywhere, GTM is completely normal. It’s also one of the most common ways these redirect campaigns get in because someone with access to the tag manager can inject a “custom HTML” tag that runs on every page without ever touching the theme. If I were the attacker, that’s where I’d hide.
So I pulled the entire GTM container and read it but it was clean. The container talked only to Google, Meta, and YouTube, exactly the advertising and analytics endpoints you’d expect on a store that runs paid campaigns. No injected redirect, no suspicious domain, no obfuscated blob pretending to be a marketing tag, so that was a dead end.
The most likely hiding spot turned out to be empty, but a ruled-out suspect is still progress. The behavior lived in the page, it didn’t come from the server, and it wasn’t in the tag manager. That left the theme itself.
Hidden in Plain Sight: A Snippet Named After an App
I pulled every external script the storefront loaded and started reading them, and I mapped every non-Shopify domain the theme referenced. Most were exactly what you’d expect on a mature store: a reviews app, a wishlist widget, a financing widget, a store-locator, a half-dozen legitimate vendors.
And then, buried in the head of the page, a small block of code that didn’t belong:
<!-- Snippet for [Wholesale] app -->
<script id="[wholesale]-config">
var Config = {
appScriptUrl: "https://wholesale.kad.systems/script/kws.js",
...
};
</script>
A theme snippet named after a wholesale-pricing app defining a small configuration object and pointing at a script hosted on kad.systems. That domain had nothing to do with Shopify and nothing to do with any of the store’s recognizable apps, and it had everything to do with our problem. A few lines further down, a loader took that config and injected the script, kws.js, onto the page.
I fetched kws.js and read it and that’s where the whole thing finally made sense.
What the Script Actually Did
The script wasn’t a wholesale-pricing tool, no sir, it was a targeted redirect-and-cloaking engine, and it was built with real care to avoid exactly the kind of investigation the owner had been attempting.
The very first thing it did was decide whether to do anything at all:
if ( navigator.webdriver === true ||
/Googlebot|bingbot|DuckDuckBot|AhrefsBot|SemrushBot|
GPTBot|ClaudeBot|Bytespider| .../i.test(navigator.userAgent) )
return;
In plain English: if you’re a search engine, a security scanner, an SEO crawler, or an automated/headless browser, quietly do nothing. That single check is why every scanner said the site was clean and why Google never flagged it. The malware refused to perform for anyone whose job was to catch it. This is called cloaking, and it’s why “the scan is clean” and “the site is clean” can be two different statements.
If you survived that filter (meaning you were a real person on a real browser) it moved to step two: it collected a fingerprint of your device (platform, browser build, hardware hints), packaged it up with the address of the page you were on, and sent it to a server back on kad.systems. Then it waited for an answer.
The answer was a URL, and the final act was simple:
navigate(j.fw); // j.fw is a URL the server just chose for this visitor
In plain English: ask a remote server where to send this particular visitor, then forcibly send the whole browser tab there. Because the destination was chosen server-side and chosen fresh for each visitor, the redirects looked “random.” One person got a fake CAPTCHA, the next got a sketchy offer page, the next got nothing at all, all controlled remotely and none of it visible in the store’s own code beyond the small loader that started it.
To confirm I understood it correctly, I replayed the request the script makes while mimicking a real browser, and the server obligingly handed back a live redirect target. The machine was still running. It just refused to run for machines.
That detail explains a lot. The owner wasn’t imagining it, and his scanners weren’t broken. He and the scanners were simply the two audiences the malware was written to fool.
The Second Realization: The App Was Already Gone
Now the obvious question: how did a wholesale-pricing snippet full of redirect malware end up in the theme?
I checked whether the “wholesale” app it was named after was actually installed. It wasn’t…it sat in the store’s list of uninstalled apps, something the business had tried at some point and removed.
This is the part that reframes the whole incident. When you uninstall a Shopify app, it’s supposed to clean up after itself, including any snippets it added to your theme. This one hadn’t, meaning the app was long gone but the block of code it had injected stayed behind: a snippet with no owner, quietly loading a script from the vendor’s server on every single page, month after month, long after anyone was paying attention to it.
And at some point, that server changed what it served. The endpoint that once delivered a harmless wholesale widget started delivering the cloaked redirector instead. The store didn’t get hacked in the dramatic sense so much as it inherited a problem. It was still faithfully calling a phone number that now belonged to someone else.
This is one of the quietest and most underrated risks in modern e-commerce. Every third-party script you add is a standing promise to run whatever that server sends you, forever, on the good faith that the server stays in friendly hands. An abandoned script from a deleted app is that promise with nobody left to keep it honest.
It Wasn’t Alone
Finding one orphaned script makes you wonder how many others are hiding so I stopped hunting for the malicious snippet and started inventorying every external script the theme still loaded, then traced each one to the file that loaded it and checked whether that file was even used anymore.
The store had a graveyard…years of trying and removing apps had left a scatter of abandoned snippets, several of them still executing on every page and still calling servers that belonged to apps the store no longer used, including two leftover scripts loading from free-tier hosting that had been dead code for who knows how long. None of the others were actively malicious the day I checked but every one of them was the same unlit fuse the kad.systems script had been before it caught: an abandoned dependency on infrastructure nobody was watching.
I fetched and inspected each of the still-live external scripts against the same malware signature: the bot-skipping, the fingerprinting, the remote-controlled redirect. They came back clean, but…“clean today” isn’t a security guarantee when the store doesn’t control the server, so they went on the removal list too. Fixing the one snippet that had turned would have solved the client’s complaint without solving the client’s actual problem, which was a habit of leaving loaded guns lying around the theme.
The Human Question: Who Had the Keys?
A malicious snippet in the theme raises an uncomfortable question: who put it there? So I moved from the code to the people, and asked to review the store’s users and access.
Early on, the owner had found and removed the account he believed was responsible, a collaborator account of the kind agencies and app developers use to work on a client’s store. On its face, that looked like a smoking gun: an unfamiliar account with the technical ability to edit theme code, sitting on a store with malicious theme code. It’s tempting to write the story right there.
I’m glad I didn’t, because it would have been wrong.
When I looked the name up, the “unrecognized” collaborator turned out to belong to a legitimate, well-established Shopify app vendor, and one of that vendor’s apps was still installed and paid for on the store. The most likely explanation was ordinary, stale access: a real vendor who’d been given collaborator rights to set up their app and was never removed afterward. Legitimate, but forgotten, which is its own kind of risk, just not the kind the smoking-gun story wanted it to be.
Could I prove that account never touched the theme? No, and here’s where I have to be honest about a limit. Shopify’s activity log on this plan only retained about a day of history. By the time I was looking, the window that would have shown when the malicious snippet first appeared and who was logged in when it did was long gone so I couldn’t definitively attribute the injection to anyone. But the weight of the evidence pointed somewhere specific: an orphaned leftover from a deleted app whose server later went bad, rather than a deliberate plant by a human with a grudge. The forgotten vendor account was a separate housekeeping problem that deserved cleaning up on its own merits, which we did, but it probably wasn’t the villain of this story. To be sure, there might not have been a villain at all, just entropy.
That’s a less satisfying ending than “we caught the hacker,” but it’s the true one, and I’d rather hand a client the true one.
Cleaning It Safely, on a Live Store
On a live storefront of a running business, mid-day, taking real orders, the fix had to be surgical and reversible.
The order of operations matters more therefore than the edits themselves:
kws.js, and then deleted the snippet file. Removing the call first means there's never a moment where the theme points at a file that no longer exists.Small, ordered, reversible changes. On a live store that discipline is what separates a clean fix from a self-inflicted outage.
Proving It Stays Gone
Removing malware and proving you removed it are two different jobs, and clients deserve the second one.
After each change I went back to the outside and re-fetched the live store, homepage, collections, and a real product page the same way a shopper’s browser would. Every page came back healthy loading and rendering normally with the add-to-cart and pricing paths intact and every page came back with zero references to kad.systems, kws.js, or the snippet that had loaded them. The loader was gone so the browser never fetched the script so the server on the other end had nobody left to redirect. The behavior was disconnected at the root (not merely suppressed).
The redirects stopped and I verified it from the outside the way the victims had experienced it rather than just asserting it from the inside.
The Part Everyone Forgets: The Backup Was Infected Too
Here’s a trap that catches a lot of well-meaning cleanups. When I checked the store’s theme library, the malicious snippet still existed in the other themes. The old backup copy the business had made earlier (and even the safety backup we’d duplicated at the start) both still carried it.
A dormant copy in an unpublished theme does nothing on its own but it’s a landmine with a long fuse: the day someone republishes an old theme to roll back a design change or recover from an unrelated mistake, the malware rides right back onto the live site and the whole cycle starts over. A cleanup that only touches the live theme isn’t finished, only postponed.
So I finished it properly: fully cleaned the live theme, made a fresh clean backup from it, and deleted the infected copies so we were left with one clean live theme, one clean backup, and nothing left in the library that could resurrect the problem.
Checking the Most Dangerous Room: Checkout
The client asked a sharp question near the end: could you also make sure nothing’s wrong at checkout? It was exactly the right question because checkout is where the card numbers are, and a redirect that annoys shoppers is a very different animal from a skimmer that quietly steals payment details. I wasn’t going to close this out without clearing it.
On Shopify Plus, checkout can be customized in a few specific ways, and I went through each. There was no legacy checkout.liquid, so no theme-level checkout code where a skimmer could live. I drove the live checkout from the outside and inspected what it loaded: it talked only to Shopify and Shop Pay with no third-party domains and none of the field-watching, data-exfiltrating patterns a skimmer needs. The store’s customer-event pixels were all legitimate, app-managed ones, with no hand-pasted custom JavaScript. The checkout configuration hadn’t been modified in months and the checkout editor itself contained only native Shopify blocks, with no injected extensions.
Checkout was clean at every layer but given what the redirect malware had already shown me about how carefully this stuff can hide, I wanted that to be a conclusion I’d actually verified rather than assumed.
What Was Underneath
Strip away the specifics and this incident was never really about one bad script. It was about the slow accumulation of forgotten trust:
Real shoppers were being redirected, but the scanners saw nothing because the script went quiet for crawlers, bots, and automated browsers.
A deleted Shopify app had left theme code behind, and that code was still loading whatever its old vendor server chose to send.
The malicious snippet was only the loudest example. Other abandoned scripts and stale collaborator access showed the same pattern: old trust relationships that had outlived their purpose.
The clean story would have been "someone hacked the store." The evidence pointed to something quieter: abandonment, stale code, limited logs, and a third-party dependency nobody still owned.
The store is clean, the malicious script and its loader along with the backups that carried it are gone, and the graveyard of abandoned scripts that would have produced the next one has been cleared. Checkout is clean, the forgotten door has been shut and the redirects that nobody could reproduce are, finally and verifiably, gone.
Stonegate Web Security investigates and remediates website security incidents like this one, including malware, redirects, blacklistings, and account compromises, on WordPress, Shopify, and beyond. If your site is doing something you can’t explain, and the scanners keep telling you everything’s fine, that gap is exactly where we start.
Related Reading
-
Case Study: The Ghost in the Machine
A Shopify scareware redirect investigation where the suspected app was gone before the evidence could be captured. -
Case Study: Blocked by Its Own Advertising
A Shopify store was flagged as dangerous because of its own advertising stack and an abandoned tracker, not a live compromise. -
Hijacked Clicks: The 2025 Redirect Trap Stealing Your Visitors' Wallets
How malicious redirects send real visitors to scam pages while hiding from scanners, owners, and automated checks. -
Content Security Policy: The Security Layer Your Website Is Probably Missing
A practical look at controlling third-party scripts before one forgotten integration can run code on every page.