The fake register: how a script tag hijacked Shopify checkouts

Bad Panda grinning behind folding table with old cash register and handwritten OPEN sign

Picture a busy store. There's a real register up front, with a real cashier and a real card reader.

Now picture Bad Panda sneaking in with a folding table, a clunky old register, and a handwritten OPEN sign. The table sits right before the real checkout. Customers think it's the real thing and pay up. Bad Panda pockets the money with a big grin. The store never sees the sale, and the customer never gets their order.

That's basically what just happened to a group of Shopify stores. No one broke Shopify's checkout. Instead, someone set up a fake register right in front of it.

What happened

In October 2026, a security researcher found a store sending shoppers to a fake checkout page. Over the next few days, more merchants reported the same thing in the Shopify Community.

Here's how the trick worked:

  1. A third-party app loaded its normal script on the storefront.
  2. That script asked the app's server for the store's settings. One setting was a "custom JS" field, which held raw JavaScript.
  3. The app ran that JavaScript right on the page. This time, the code was malicious.
  4. The code watched the Checkout button. When a shopper clicked it, they were sent to a look-alike checkout on another domain.
  5. The fake page collected card details, then sent the shopper to the real checkout to finish their order.

The code also set a flag in the browser so it only fired once per session. That's why merchants who tested their own checkout often saw nothing wrong.

Six-step flow: shopper clicks Checkout, app script fetches settings, settings include custom JS, app runs it and takes over button, shopper lands on fake checkout, card details go to attacker. Real Shopify checkout never reached

Researchers tied the redirects to these domains (defanged so they aren't clickable):

So who put the code there?

That's still being sorted out, and we won't guess. It could have been added through access on the app's side, or through a merchant or staff login that could edit the app's settings. For this post, the "who" matters less than the "how." Any system that stores JavaScript as data and runs it on a storefront can become a fake register. Whoever controls that data controls your checkout button.

Why attackers go after the button, not the checkout

Shopify's checkout doesn't let anyone run arbitrary JavaScript. The payment fields are hosted by Shopify, and checkout customizations are limited to sandboxed extensions.

So attackers can't tamper with the real register. Instead, they go after the hallway right before it: the storefront, where the rules are much looser. If they can control what happens when someone clicks Checkout, they never have to touch the checkout at all.

Which Shopify surfaces are at risk?

Not every place you can add code is equally exposed. The key questions are:

SurfaceRuns any JS?Can touch the page?Versioned?Risk
Script tags (ScriptTag API)Yes, from any URLYesNo, and they're hidden from the theme editor🔴 High
Theme code (theme.liquid, sections, snippets, Custom Liquid blocks)YesYesLimited, with per-file history only🔴 High
App-side "custom JS" settings run with eval, new Function, or injected <script>Yes, and it's fetched at runtimeYesOnly if the app builds it🔴 High
App proxiesYes, served from the app's serverYes, on the store's own domainNo, it's whatever the server returns🟠 Medium
Theme app extensions (app blocks and app embeds)Yes, from bundled assetsYesYes, through app versions you can roll back🟠 Medium (🔴 High if the asset loads remote code)
Custom pixelsYes, written by the merchantNo, it runs in an isolated iframeNo🟡 Low to medium
Web pixel app extensionsYes, in a web workerNoYes🟢 Low
Checkout UI extensionsOnly Shopify's componentsNo, it runs in a sandboxYes🟢 Low
Customer account UI extensionsOnly Shopify's componentsNo, it runs in a sandboxYes🟢 Low
Shopify FunctionsNo, it's server-side WebAssemblyNoYes🟢 Very low
checkout.liquid and additional scriptsRetired——⚪ Retired
Headless storefronts (Hydrogen or custom)YesYesDepends on your pipelineDepends on you

A few notes on the table:

Tips for app developers

If you build Shopify apps, here's how to make sure you're never the one handing out folding tables.

Don't run JavaScript from your database

Even if a merchant typed it in. Even if it's "their" code. Once you run a string from your database with eval, new Function, innerHTML, or a <script> tag you build on the fly, your database is part of every storefront you're installed on. Anyone who can write to that field can change every checkout button, whether through a stolen merchant login, a support tool, or a leaked API key.

Offer settings, not code

Most "custom JS" boxes exist to change a color, move an element, or fire an event. Turn those needs into real settings. Use theme app extension block settings, metafields, or a typed JSON config that your bundled code reads. Data should describe behavior. It shouldn't be behavior.

Ship code only through app versions

Put your storefront JavaScript in theme app extension assets and release it with shopify app deploy. You get a version history, a clear diff, and a one-click rollback. If you're still using script tags, move now. You have until March 1, 2027.

If you must accept custom code, treat it like a deploy

Sometimes you really need it. When you do:

Escape everything else

Button labels, product names, and any text a merchant enters should be inserted as text, never as HTML. One unescaped label is all an attacker needs to slip in a script.

Never redirect to a domain you don't control

Your app should never send a shopper to a URL that came from settings, and it should never take over the Checkout button. If you need a redirect, check it against an allowlist of your own domains.

Run Shopify's security check before every release

On October 15, 2026, Shopify released an app security check in Shopify CLI and the Shopify AI Toolkit. It scans your app's code, extension config, and dependencies on your own machine. Then it reports each issue with the file, line, severity, and a suggested fix.

Several of its checks cover the mistakes behind a fake register:

Run it in two ways:

A clean report doesn't mean your app is safe. There's no check named "runs JavaScript from the database," so the agent review and your own code review still matter. But it's free and fast, and it catches the common mistakes. Shopify recommends running it on every release. You can make sure that happens by adding it to your CI. Here's our guide to running the check in GitHub Actions.

Watch for the fake register

Test checkout on a schedule with a fresh browser session every time, since once-per-session tricks hide from repeat visits. Our open-source check does this, and you can run it in CI. Alert on outbound requests to domains you don't recognize. If you run a headless storefront or app proxy pages, add a Content Security Policy that only allows scripts from sources you trust.

Tips for merchants

You don't need to read code to protect your store. You just need to know where to look.

Check your own store

Testing checkout by hand is a good start, but it's easy to miss a trick that only fires once per session. So we wrote a free, open-source check that your AI assistant can run for you.

It acts like a first-time shopper. Your assistant opens your store in a brand-new browser tab, adds a product to the cart, and clicks Checkout. Then it tells you where it ended up and looks for the known signs of this attack. It stops at the checkout page and never enters or submits anything.

You'll need an assistant that can use a web browser. Pick yours below.

You'll need Claude in Chrome, which comes with paid Claude plans.

  1. Open Chrome and go to your store.
  2. Open Claude in Chrome from the toolbar.
  3. Paste the prompt below. Replace https://yourstore.com with your store's address.
Check my Shopify store's checkout for a phishing redirect. My store is https://yourstore.com. Only test this store.

Do this 3 times. Each time, start in a brand-new browser tab:
1. Open my store.
2. Add any in-stock product to the cart, then go to the /cart page.
3. Click the Checkout button once.
4. Wait a few seconds, then write down the full address of the page you land on. Don't type anything on that page.
5. Go back to my store in the same tab. If you can run JavaScript, run Object.keys(sessionStorage) and write down any key that starts with __ctf_.
6. If you can see network requests, write down any request to biliing.top, checkout-confirm.top, or order-confirm.top.

Then give me a result:
- PASS if every run landed on my store's domain or a Shopify domain (shopify.com, myshopify.com, shop.app), with nothing found in steps 5 or 6.
- FAIL if any run landed on a different domain, or steps 5 or 6 found something. Tell me exactly what you saw.
- INCONCLUSIVE if you couldn't reach checkout. Tell me what got in the way.

If it's a FAIL, tell me to contact Shopify Support right away, and to check every app's settings for "custom JS" or "custom code" fields.

Using Claude Code? Install the full check, which also flags apps that send custom JavaScript to your store:

claude plugin marketplace add ctrlaltdylan/storefront-skills
claude plugin install storefront-security@storefront-skills

Restart Claude Code, then run /check-checkout https://yourstore.com.

You'll get one of these results:

The skill and prompt are free and open source on GitHub.

A few things to keep in mind:

Close the fake register

Bad Panda's folding table only works when no one's watching the hallway. The best fix isn't a bigger lock on the real register. It's making sure nothing gets set up in front of it without someone noticing.

For developers, that means shipping code through versioned, sandboxed extensions, never running code from a database, and running Shopify's security check on every release. For merchants, it means knowing what's running on your storefront and testing your checkout the way a first-time shopper would.

The real register is safe. Make sure it's the only one in the room.