AppScan
For Stripe-powered apps

Prices from the server. Webhooks verified.

Money is where a small mistake stops being small. The amount that came from the browser, the webhook that trusts whoever calls it, the secret key in a public bundle: each is one line, and each is a rule.

01The situation

Sound familiar?

The price came from the browser

The checkout posts an amount and the server charges it. Anyone who can edit a request can name their own price.

A webhook that trusts its caller

A handler that reads the event body and marks the order paid, without checking the signature, marks any order paid for anyone who asks.

A secret key in a public bundle

sk_live in JavaScript is the whole account. It happens because the publishable and the secret key look alike at 2am.

A refund route without an admin check

The guard was there. A refactor moved it. Nothing failed, because nothing was watching for a protection to disappear.

02What changes

Traced, verified, and kept that way.

Taint analysis follows request input to the payment call. Webhook handlers are read for a signature check before the body is trusted. Both become invariants, so a future push that breaks them fails its pull request.

  • Price traced from request to charge

    An amount that flows from the request into the payment call is a finding; that prices come from the server is an invariant re-checked on every push. source.billing.price-from-client-input

  • Signature or nothing

    A webhook handler that uses the body without a visible verification - constructEvent, a timing-safe compare, a provider SDK - is named, and the invariant holds only when every handler shows one. source.webhook.missing-signature-verification

  • Three Stripe key shapes

    The live secret, the test secret and the publishable key are told apart. The first two in public JavaScript are findings; the third is expected there and never reported as a leak. runtime.javascript.credential-exposed

  • A protection removed is a finding

    An admin check that disappears from a refund route is raised on its own, on the pull request that removed it. 4 more rules watch paid endpoints that are unauthenticated, unlimited or unthrottled. source.authorization.protection-removed

checkout-app · invariants5 / 6 holding
  • Webhooks verify provider signatures
    Holdingwebhooks.signatures-verified
  • Prices come from trusted backend data
    Holdingbilling.prices-from-backend
  • Reads are scoped to the tenant
    Brokentenant.reads-scoped:<Model>
  • Mutating routes require authentication
    Holdingauth.mutations-guarded
  • Admin operations require admin permission
    Holdingauth.admin-routes-require-admin
  • CORS uses an explicit origin allowlist
    Holdingcors.explicit-allowlist
03A week with it

What the first week looks like

  1. 01
    Scan the deployed site

    The report says whether a Stripe key is in your JavaScript, which kind, and whether the payment provider is in test mode on a production host.

  2. 02
    Connect the repository

    The inventory finds the payment calls, the webhook handlers and the refund routes, and derives the two invariants.

  3. 03
    Read the first findings

    Each says what is wrong, why it matters, how it would be abused, how to fix it and how the fix is verified.

  4. 04
    Let the check watch the refactor

    The pull request that drops the admin check fails. The one that restores it passes.

Where to start
Pro$29 / month

The rules about money live in source, so they need the repository. Pro connects it and re-checks the invariants on every push.

  • One site with its repository connected
  • The billing, webhook and cost-abuse source rules
  • Invariants re-checked on every pull request
  • Fix with AI as a draft pull request
Create a free account

Start on Free, which does not expire. Upgrade when it pays for itself.

Find out where the money is exposed.

The free scan reads the bundle. The repository reads the rest, and the invariants keep it read.