Demo, all content is generated
Question

People can get the paid feature by just going to /success

Solved · 2608 views · asked by jakeypoo · edited

Found this by accident. My Stripe checkout success_url is /success, and the success page sets the user to premium in Firestore. So if you just type mysite.com/success you get premium for free. lol.

How are you supposed to know the payment really happened?

What I’ve tried

Asked Cursor to check if payment went through on the success page, it added a check for a ?paid=true query param, which I can also just type.

Comment
haha the ?paid=true fix is peak AI wes_codes · edited

3 answers

Marked as helpful by the asker
tobiasw · edited

The success page should never grant anything. Two correct options:

Best: webhook. Handle checkout.session.completed (or subscription events for subscriptions) on your server, verify the signature, and set premium there. The success page just says "thanks, finishing up" and polls your own DB until premium shows.

Also fine as a UX shortcut: verify the session server side.

success_url: "https://mysite.com/success?session_id={CHECKOUT_SESSION_ID}"

Stripe fills in the real id. Then on the server:

const session = await stripe.checkout.sessions.retrieve(sessionId);
if (session.payment_status === "paid" && session.client_reference_id === currentUser.id) {
  // grant
}

The client_reference_id check (set it to your user id when creating the session) stops someone from reusing another person's session id.

And whatever grants premium must not be writable from the client. If the browser can write premium: true to Firestore, your security rules need fixing too.

Comment
the firestore rules part... yes users can write their own doc. fixing that first jakeypoo · edited
For Firestore: make premium fields writable only by the Admin SDK (your server). In rules, deny client writes that touch premium: !request.resource.data.diff(resource.data).affectedKeys().hasAny(['premium']). sven_fire · edited
Done both. webhook + rules. Thanks guys jakeypoo · edited
kofi_mensah · edited

Rule of thumb for anything payment related: the browser is the customer's device, not yours. Anything it says is a suggestion.

Comment
writing this on a post-it jakeypoo · edited
katja_s · edited

And check premium server side on every premium action, not only when rendering the page. Hiding a button isn't access control.

Comment