./lock-the-can --start

Lock the Can.

14 checks stand between your app and a raid. Work through each zone, seal what you've handled, and Bandit walks away empty-pawed. Progress saves in your browser.

status: 0/14 sealed

cd ./front-door

OAuth & install — how merchants let you in.

  • CRIT Verify the HMAC on the OAuth callback

    Shopify signs the redirect back to your app. Skip the check and Bandit can forge an install and hand you a shop domain he controls.

    → fix: Recompute HMAC-SHA256 over the sorted query string (minus hmac) with your client secret; reject on mismatch with crypto.timingSafeEqual.

  • CRIT Validate the OAuth state nonce

    Without a random, per-request state you have no CSRF protection on install — an attacker can trick a merchant into authorizing under the wrong context.

    → fix: Generate a random state, store it in a signed cookie/session, and reject the callback if it does not match.

  • WARN Request the minimum OAuth scopes

    Every extra scope is extra loot the day you get breached. `write_orders` you never call is pure downside.

    → fix: List only the scopes each feature needs; re-audit on every release and drop the ones you stopped using.

  • WARN Allowlist your OAuth redirect URLs

    Open redirects during install can leak the authorization code to an attacker-controlled host.

    → fix: Register exact redirect URIs in the Partner Dashboard; never build them from untrusted input.

cd ./mail-slot

Webhooks — anything the outside world POSTs to you.

  • CRIT Verify X-Shopify-Hmac-Sha256 on every webhook

    Webhook URLs are not secret. Unverified, any POST that looks like Shopify gets trusted — Bandit forges a paid order.

    → fix: Hash the raw request body with your app secret and compare with timingSafeEqual before parsing. See the Dumpster Dive.

  • WARN Confirm the shop domain matches an install you know

    A valid signature only proves it came from Shopify — not that this shop belongs to the session acting on it.

    → fix: Cross-check X-Shopify-Shop-Domain against your stored installs before mutating anything.

  • INFO Handle the mandatory compliance webhooks

    customers/redact, shop/redact, and customers/data_request are required. Ignoring them risks app rejection and privacy violations.

    → fix: Subscribe and implement all three; actually delete/return the data, do not just 200 them.

  • WARN Purge tokens on app/uninstalled

    A stale access token for an uninstalled shop is a live credential with no owner watching it.

    → fix: On uninstall, revoke/delete the stored token and any cached merchant data.

cd ./the-safe

Secrets & tokens — the stuff worth stealing.

  • CRIT Keep access tokens server-side only

    A token in localStorage or a client bundle is a permanent Admin credential Bandit lifts with one DevTools glance.

    → fix: Store tokens encrypted at rest server-side; the frontend uses short-lived session tokens via App Bridge.

  • WARN Store API secrets in a secret manager, not code

    A client secret committed to git is compromised forever, even after you delete the commit.

    → fix: Load secrets from env/secret manager; scan the repo history and rotate anything that leaked.

  • CRIT Verify App Bridge session-token JWTs

    If your backend trusts a session token without verifying its signature and expiry, it trusts a forgery.

    → fix: Validate the JWT signature with your secret, check aud, dest, and exp on every request.

cd ./the-perimeter

Embedded UI & merchant input — your walls.

  • WARN Set Content-Security-Policy: frame-ancestors

    Embedded apps must scope who can frame them or you open the door to clickjacking inside the admin.

    → fix: Send frame-ancestors https://[shop].myshopify.com https://admin.shopify.com per request.

  • WARN Validate & escape all merchant-supplied input

    Metafields, product titles, and form fields are attacker-controllable. Reflect them raw and you have stored XSS.

    → fix: Validate on input, escape on output, and use parameterized queries — never string-build SQL/HTML.

  • INFO Respect the API rate limit (handle 429s)

    Not security-critical, but a hammered app gets throttled and degrades — and retry storms mask real abuse.

    → fix: Honor the leaky-bucket headers, back off on 429, and queue bulk work.