# Paid access is granted from a verified webhook or a server-side checked session, never from the success page alone

- **Pattern:** `ab-002651` (`stripe-billing-integrity.entitlement-integrity.fulfillment-has-verified-webhook-path`)
- **Severity:** critical
- **Lifecycle:** active
- **Last modified:** 2026-10-05
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002651
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002651)

## 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.

```ts
// success page server action
const session = await stripe.checkout.sessions.retrieve(sessionId)
if (session.payment_status !== 'paid' || session.client_reference_id !== user.id) notFound()
```

## Detection

**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](https://auditbuffet.com/audits/stripe-billing-integrity). The pattern definition, severity rationale, and remediation remain openly licensed under CC-BY-4.0.

## External references

- cwe CWE-602
- cwe CWE-284
- owasp A01
- owasp A08
- external stripe-checkout-fulfillment — https://docs.stripe.com/checkout/fulfillment?payment-ui=stripe-hosted

Taxons: access-control, data-integrity

HTML version: https://auditbuffet.com/patterns/ab-002651
