Paid access is granted from a verified webhook or a server-side checked session, never from the success page alone
Why it matters
Stripe Checkout redirects the customer to your success_url after payment, and Stripe's fulfillment guide states that the redirect alone is not a reliable signal: customers can close the tab before it loads, and the URL can be requested by anyone. Fulfillment must be triggered by a verified checkout.session.completed (or invoice.paid) webhook, or by a server-side stripe.checkout.sessions.retrieve(session_id) that confirms payment_status === 'paid'. AI-built apps routinely do the opposite: the success page handler reads ?session_id= and flips user.plan = 'pro' without calling Stripe, so visiting /success?session_id=anything grants the subscription; or the success page makes a client-side call to an endpoint that trusts the supplied id. This is CWE-602 (Client-Side Enforcement of Server-Side Security) and CWE-284 (Improper Access Control), OWASP A01 and A08. It is distinct from client-supplied plan or status writes, which belong to the No Payment Bypass API check in this audit; this check is about the missing trusted server-side path that confirms a payment actually happened. Outcome: free access to every paid feature for any visitor who learns the success URL pattern.
Severity rationale
Critical because paid state written from a return URL or a client-supplied `session_id` can be reached by any user without paying, so the paywall is bypassable by visiting a URL.
Remediation
Grant paid state only from a trusted server-side path. Primary path: handle checkout.session.completed (and invoice.paid for subscriptions) in a verified webhook and write the entitlement there, keyed by the session's client_reference_id or customer id. If the success page also needs to show status immediately, have it call your server, which runs stripe.checkout.sessions.retrieve(sessionId) and proceeds only when payment_status === 'paid' and the session belongs to the signed-in user; the page itself writes nothing.
// success page server action
const session = await stripe.checkout.sessions.retrieve(sessionId)
if (session.payment_status !== 'paid' || session.client_reference_id !== user.id) notFound()
DetectionPro
The detection method for this pattern is part of AuditBuffet Pro.
The full method (what to look for, pass and fail conditions, and how results get reported) ships inside the Stripe Webhooks & Billing Integrity audit. The pattern definition, severity rationale, and remediation remain openly licensed under CC-BY-4.0.
External references
- cwe · CWE-602 — Client-Side Enforcement of Server-Side Security
- cwe · CWE-284 — Improper Access Control
- owasp:2021 · A01 — Broken Access Control
- owasp:2021 · A08 — Software and Data Integrity Failures
- external · stripe-checkout-fulfillment — Stripe docs: Fulfill orders with Checkout (webhook-driven fulfillment)
Taxons
History
- 2026-10-05·v1.0.0·Initial authoring — fulfillment must come from a verified webhook or server-side retrieve with payment_status check, not the success page; client-supplied plan writes stay with ab-002265.·by stripe-billing-integrity-v1-build