The check that passes but doesn't protect
When a Shopify app team asks Alex Tarantey to review their app before a big merchant or a security review, the first thing he examines is the work developers had already marked as fine. He said "The checks that exist but don't protect what the team thinks they protect."
Almost every team tells him webhooks are verified, the app proxy signature is checked, and rate limiting is in place. Usually, that's true. The problem is what happens after the check.
He shared three examples from recent reviews, without naming the apps. In one, a signed compliance webhook could wipe another account: the customers/redact handler verified the HMAC correctly, but then looked up the user by email across the whole platform rather than only among customers of that store. An attacker could install the app on a free development store, create a customer with the victim's email, and request data deletion. About ten days later, Shopify sends a genuine, correctly signed webhook, and the victim's account is anonymized. "The signature was valid. The scope was wrong."
In another, a verified app proxy request acted on another store. The middleware checked the app proxy signature on the query string, but the controller read shop through Laravel's validate(), which merges the request body with the query, and the body takes priority. A POST through the attacker's own store proxy, with a valid signature and {"shop": "victim.myshopify.com"} in the body, created and released bookings in the victim's store.
A third example involved a rate limit that limited everyone at once. A storefront password gate limited failed attempts per IP, but requests arrived through Shopify's app proxy, Cloudflare, and two reverse proxies, so the app effectively saw the same internal IP for every visitor. Five wrong guesses from anyone locked the gate for all shoppers.
None of these would show up in a scanner or a happy-path test. Each endpoint looks correct in isolation. "So the first thing I check is where the store, the user and the client identity come from after verification, and whether they come from the verified part of the request."
The same applies to infrastructure. During the PHP2Shell patching in October, "patched" servers were often still running vulnerable code because PHP-FPM and queue workers hadn't been restarted. php -v on Ubuntu doesn't show the fix either, so Tarantey checks the package changelog instead.
What general guides leave out
General app security guides think in users. In a Shopify app, the tenant is the store, and the store's identity reaches the app only through Shopify-specific channels: the session token, the webhook HMAC, and the app proxy signature.
Tarantey keeps running into verified requests whose data isn't scoped to the verified store. "A valid signature tells you which store sent the request. It doesn't make every field in the payload trustworthy, and it doesn't limit what your code does with them."
He also sees webhook verification that gets "fixed" by switching it off. When the HMAC is computed over re-encoded JSON instead of the raw body, legitimate deliveries fail, and the tempting fix is to disable the check. A generic checklist says "verify signatures," but it doesn't say verification has to run on the raw bytes Shopify signed.
Infrastructure behind the app proxy is another blind spot. Requests reach the server from Shopify, not from the shopper's browser, usually through a CDN and reverse proxies. Anything keyed on the client IP—rate limits, logging, abuse detection—needs a deliberate decision about which IP it actually sees.
Old servers with no clean upgrade path also come up. Some backends patched for PHP2Shell were on Ubuntu 20.04, where the only fixed package is PHP 7.4 through Ubuntu Pro. That closes the CVE but leaves the app on a PHP version upstream stopped supporting in 2022. The honest fix was upgrading to 22.04 and moving to PHP 8.1, which breaks things guides never mention: FPM socket paths (instant 502), php7.4 hardcoded in Supervisor units and cron jobs, and OpenSSL 3 rejecting old keys used by outbound integrations.
After any infrastructure change, Tarantey re-tests install, OAuth, webhooks, and billing, because a broken webhook or OAuth flow quietly hurts every merchant at once.
What founders decide too quickly
Before a launch or a merchant security questionnaire, Tarantey sees three common mistakes.
First, "it's patched / it's scanned, so we're fine." "A version string or a scanner report isn't evidence that the fix is running, or that one store can't reach another store's data." Scanners are good at dependencies and known misconfigurations, but they don't understand which store a record belongs to, and none of the issues above would have shown up in one.
Second, picking the fastest fix instead of the one that will last. With PHP2Shell, enabling Ubuntu Pro on an old server takes an hour, but it keeps the app on an unsupported PHP, so the next critical bug lands on the same problem. Tarantey prefers a planned upgrade: snapshot, rehearse on a copy, run the test suite on the new PHP, then upgrade production.
Third, not keeping evidence. Questionnaires and reviewers increasingly want to see that issues were found, fixed, and checked, not just to hear "we have no issues."
His advice for founders:
- Test early enough to fix and retest before the date.
- Test with two development stores and swap identifiers between them, covering Shopify-specific paths: install and uninstall, OAuth, webhooks including compliance webhooks, the app proxy, and billing—not only the admin UI.
- Record what was checked and how: package versions, changelog checks, commands, dates, and the retest result. Not
php -vscreenshots. - Have a plan with a date for anything still on end-of-life software. For example, Ubuntu 22.04 standard support ends in May 2027, and PHP 8.2 loses security support at the end of 2026.
For teams preparing for a review, Tarantey's approach is to look past the presence of a check and ask what the verified data is allowed to do. More on his work is available at AppPerimeter.
