A customer finishes checkout and the payment provider sends their browser back
to /success. Your app shows a green check. That is useful user experience. It
is not evidence that the order was paid.
The browser controls the URL it visits. It can revisit that URL, change its
query string, share it, or open it without completing checkout. If visiting
/success?paid=true creates access, the paywall is a URL convention—not an
authorization boundary.
The launch check is simple:
A success-page visit must create zero entitlements.
Reproduce the failure
The ShipTested public teaching harness contains this deliberately unsafe
handler:
export function vulnerableHandleSuccessPage({ query, entitlements }) {
const params = new URLSearchParams(query);
if (params.get("paid") === "true") {
entitlements.push({
userId: params.get("user_id"),
source: "browser-redirect",
});
}
}
Nothing here establishes that a payment provider accepted an order. Both
paid and user_id came from the browser. A visitor can type:
/success?paid=true&user_id=user-b
The exploit test calls the handler with that query and verifies that it creates
an entitlement. Run it with:
npm run test:failures
One naming detail matters: the exploit test passes when it successfully
reproduces the vulnerable behavior. It is evidence of a failure, not launch
approval.
This example is intentionally small. It does not claim that every checkout
integration has this bug. It gives you a deterministic way to recognize and
test one unsafe pattern in your own app.
State the invariant before choosing the fix
The fixed example makes the success page informational only:
export function handleSuccessPage() {
return {
status: 200,
access: "pending-verified-webhook",
};
}
Its launch test supplies the same hostile query and checks two outcomes:
assert.equal(result.access, "pending-verified-webhook");
assert.equal(entitlements.length, 0);
The model still returns status 200 and pending-verified-webhook. It
demonstrates an informational response without creating access, regardless of
the supplied query parameters.
Run the green checks with:
npm run test:launch
The checkout check passes only when a success-page visit creates no
entitlement.
What should create access instead?
Put the authority on the server side. A common payment flow looks like this:
- An authenticated server endpoint maps an internal plan name to an allowlisted provider product or variant.
- The server attaches the signed-in internal user ID as checkout metadata. The browser does not choose the owner, price, or variant as trusted data.
- The success page shows a pending or processing state. It may read current entitlement state, but it does not write it.
- A server-side verified payment event creates the entitlement. The planned full ShipTested reference flow uses a signed webhook whose event type, paid status, store, variant, test mode, order ID, and internal user ID are checked before a state change.
- Duplicate deliveries become no-ops through a database-enforced boundary such as a transaction and appropriate unique constraints.
A signed webhook is not the only possible provider architecture. Some
integrations retrieve and verify a checkout session server-to-server after the
redirect. The invariant stays the same: browser-controlled redirect data does
not grant access by itself.
The public checkout example proves only the negative redirect invariant. It
does not yet contain the server-created checkout endpoint. That endpoint,
durable database constraints, refund handling, and provider-specific
integration belong to the full reference repository.
The public webhook model checks only that the custom user ID is a non-empty
string. A production flow must resolve it to an eligible internal record before
granting access.
Run the check against your staging app
Use a staging environment and test data:
- Copy the checkout success URL.
- Open it in a fresh session without placing an order.
- Refresh it several times.
- Add or change parameters such as
paid,status,plan,variant, anduser_id. - Inspect the entitlement table or access-control result after every attempt.
The passing result is zero new access. A message saying “payment processing” is
fine. A new entitlement, paid role, license, credit balance, or protected
download is a failure.
Then complete a sandbox purchase and verify the separate server-owned
fulfillment path. That second step matters, but it is outside what the small
redirect harness proves.
The boundary of this proof
The repository is a dependency-free Node.js teaching harness with in-memory
state. It demonstrates an unsafe redirect and a fixed invariant through
executable tests. It is not a production payment integration, a security audit,
or a certification. Passing this one test does not make an app ready to sell.
It does remove one dangerous ambiguity: the browser may report where it landed.
It does not decide who paid.
Run the broader launch-readiness checklist
Sources: Lemon Squeezy — Taking Payments,
Create a Checkout API,
and Passing Custom Data.
ShipTested uses AI heavily to produce this material and verifies the technical
examples by running them. There are no fake testimonials and no guaranteed
outcomes — the point is that you run the tests yourself instead of trusting an
author.
Top comments (0)