Skip to main content
Aimeice Tech
ShopifySecurityWeb Applications

Shopify App Security: Cross-Shop Isolation Before Delisting

Shopify is delisting apps that get breached. What we audited for a client's Shopify app: cross-shop access, non-expiring tokens, and secrets leaking into logs.

Andrii Nasadchuk, Founder & CEO · November 07, 2026 · 6 min read

Shopify has been delisting apps that got hacked. Hundreds of them this autumn.

For an engineer, a breach is a bug with a postmortem. For an app founder, a delisting is the end of the business: no new installs, existing merchants uninstalling, and a reputation in the App Store that does not come back. That asymmetry is why Shopify app security deserves a different kind of attention than feature work. It is not a backlog item. It is the floor the whole company stands on.

Last month we did the unglamorous version of this work for one of our clients, a Shopify app with paying stores. Nothing in it would look good in a demo. Here is what we did, why, and the checks you can run on your own app this week.

Why Shopify apps fail in the same few ways

Almost every Shopify app is multi-tenant by design. One codebase, one database, many shops. Every request carries an implicit question: which shop is this, and is this caller allowed to touch it? When an app gets that question wrong even once, one merchant can read or change another merchant's orders, customers, or settings.

The failure modes we see over and over are not exotic:

  • Cross-shop access. An endpoint trusts a shop or id parameter from the client instead of the verified session, so changing one value in a request returns someone else's data.
  • Long-lived credentials. Offline access tokens that never expire, stored in plain columns, copied into background jobs and admin tools. One leak and the attacker has standing access to a store until someone notices.
  • Secrets in logs. Tokens, API keys, and webhook payloads written to application logs, error trackers, or job queues, where they live for months and are readable by every tool and person with log access.

None of these show up as failing tests. The app works perfectly for every merchant, right up until someone tries it on purpose.

What we actually did for the client

The scope was three pieces of work. Each one is boring on its own. Together they close most of the doors that get apps delisted.

Audit

Every endpoint checked for cross-shop access, including webhooks, jobs, and internal admin routes.

Rotate

Moved off non-expiring tokens to short-lived access with refresh, encrypted at rest.

Prove

Automated tests that fail the build if any secret ever lands in a log line.

1. An endpoint-by-endpoint cross-shop audit

We listed every route the app exposes: the embedded admin UI, the public API used by the storefront extension, webhook receivers, background job entry points, and the internal support dashboard everyone forgets about. For each one we asked the same three questions:

  1. Where does the shop identity come from? It should come from a verified session token (for embedded app requests), a valid HMAC (for webhooks and app proxy calls), or a server-side job argument that was itself derived from one of those.
  2. Is every database query scoped by that shop? Not "usually", every query. A lookup by record ID alone is a cross-shop leak waiting for a curious merchant.
  3. Can a valid session for shop A reach data for shop B? We did not answer this by reading code. We wrote a test for it.

The fix pattern is simple and worth enforcing structurally: resolve the current shop once, from the verified source, and make every data access go through a scope that starts from it. In a Rails app that means current_shop.orders.find(id) instead of Order.find(id). The second form is the one that ends up in incident reports.

2. Moving off non-expiring tokens

Historically, a Shopify offline access token lived until the merchant uninstalled the app. That is convenient, and it is also exactly what you do not want sitting in a database dump. Shopify now supports expiring offline tokens with refresh, so we migrated the app to short-lived access tokens, stored refresh tokens encrypted at rest, and made token refresh a single code path that every API client goes through.

The trade-off is real: more moving parts, and a refresh failure now means a failed sync instead of a silent success. We handled it with retries, clear alerting when a shop's refresh fails, and a re-auth prompt in the admin UI. In exchange, a leaked token is useful to an attacker for minutes or hours, not years.

3. Tests that no secret ever lands in logs

"Don't log secrets" is a policy. Policies decay. We turned it into a test suite. The tests run representative flows (install, OAuth callback, webhook receipt, API errors, job failures) with known fake secrets, capture everything written to the logger and the error tracker, and fail if any of those values appear. We also added parameter filtering for token, key, and signature fields so the default behaviour is safe even when someone adds a new log line in a hurry.

The first run found two leaks: an error handler that logged full request headers, and a background job that logged its arguments on retry. Neither was anyone's fault. Both would have been in a breach report.

Nothing to demo. But the app is still in the store, and the founder sleeps.

The three checks we start with

If you run a Shopify app and nobody outside your team has ever looked at cross-shop isolation, these are the first three things we would check. You can do them yourself in an afternoon.

  1. The two-shop test. Install your app on two development stores. Log in as shop A, capture a few API requests from the browser dev tools, and replay them with IDs or shop domains from shop B. Any response other than a 403 or 404 is a finding.
  2. The token inventory. Find every place an access token is stored or passed: database columns, caches, job arguments, environment variables in staging, support tooling. Ask whether each one is encrypted, whether it expires, and who can read it.
  3. The log grep. Search the last 30 days of logs and your error tracker for the prefix Shopify uses on access tokens (shpat_, shpca_, shpua_) and for your own API secret. If you find a hit, you also found a process gap.

None of this replaces a proper audit, but it tells you quickly whether you have a problem worth paying for.

Why an outside look matters

The team that built an app is the team least likely to find its isolation bugs. They know which parameters are "supposed" to be safe, so they never try changing them. An outside review costs a few days. A delisting costs the business. For an app with paying stores, that is not a close call.

Running a Shopify app with paying stores?

We review cross-shop isolation, token handling, and logging, then fix what we find. Message us and we will walk you through the three checks on your own app.

Book a security review

Written by

Andrii Nasadchuk

Founder & CEO

Product engineer. Leads strategy, sales and delivery.

Start here

Tell us what should grow next

Send what you sell and where clients come from now. Within one business day: which agent first, what number it moves, what it costs.

Or run the growth audit first. No email needed to see your score.