The client sets the price, role, or plan
If the browser can send amount, plan, or role and the server saves it, users will send the values they want.
Last updated
Shows up in: Stripe, Next.js, Supabase, Firebase
What it is
The client is a suggestion. A checkout request that includes amount: 100 in cents, or priceId copied from a hidden field the user can edit, is the user choosing what they pay. A profile update that includes role: "admin" or plan: "pro" is the user choosing their privileges.
The server should look up the price from a list you control. In Stripe, that list is a Price you created in the Dashboard, referenced by id. Your server decides which price id is allowed for this action. The webhook, after signature verification, is what marks the account paid. The success URL is a page you show. It is not proof of payment. See unverified Stripe webhooks.
Mass assignment is the general name for "the update copies the JSON onto the row." ORMs will happily set role if you pass ...req.body.
How to check in 5 minutes
Search for
amount,price,plan,role, andisAdminin Route Handlers and Server Actions. If the value comes fromrequest.json()orformDataand is written to Stripe or the database, that is the bug.Create a checkout on a test account and change the amount in the request before it is sent. If the Stripe session opens for the edited amount, the server trusted you.
Update your own user row with an extra
rolefield. Read it back. If it stuck, the update is not filtering fields.Load the post-checkout success page while logged in as someone who has not paid. If it grants the plan, the page is the bug. The webhook should be the grant.
How to fix
Stripe
const prices = {
pro: process.env.STRIPE_PRICE_PRO!,
} as const;
export async function POST() {
const user = await getCurrentUser();
if (!user) return new Response("Unauthorized", { status: 401 });
const session = await stripe.checkout.sessions.create({
mode: "subscription",
line_items: [{ price: prices.pro, quantity: 1 }],
success_url: "https://your-app.example/account",
cancel_url: "https://your-app.example/pricing",
client_reference_id: user.id,
});
return Response.json({ url: session.url });
}
The price id lives in the server environment or a server-side map. Quantity, if the user may choose it, is checked against a range you set. The webhook handler, after constructEvent, writes the plan. The success page only displays status it reads from your database.
Next.js
Assign fields explicitly:
await db.user.update({
where: { id: user.id },
data: { name: form.name },
});
Do not data: await request.json().
Supabase
A policy with check (auth.uid() = id) still allows the user to set other columns on their own row. Revoke column updates they should not have, or use a trigger that rejects changes to plan and role. RLS that only checks the owner does not check the price.
Firebase
allow update: if request.auth.uid == resource.data.userId
&& request.resource.data.role == resource.data.role
&& request.resource.data.plan == resource.data.plan;
Deny changes to privileged fields in rules. The Admin SDK can set them after a verified webhook.
Copy-paste prompt
Paste this into Cursor, Claude Code, or whichever agent is editing the repo. Read the diff before you merge it.
You are stopping the client from setting price, plan, or role. Do not add features. 1. Find checkout code. The amount or price must come from a server-side price id, not from the request body. Quantity must be range-checked if it comes from the client. 2. Find user updates. Replace object spreads of request JSON with an explicit field list that cannot include role, plan, isAdmin, or credits. 3. Grant paid plans only from a Stripe webhook whose signature you verified. Do not grant them from the success page or from a client call. 4. If Supabase or Firebase rules let a user update their own plan column, deny that column. Show each write you narrowed and where a plan is now granted.
Questions
We show the price in the page. Is that the bug?
No. Displaying a price is fine. Charging whatever number comes back from the browser is the bug.
Can I trust a price id the client sends if price ids are not secret?
Only if the server checks that id against an allowlist for that action. A client can send a different price id that exists in your Stripe account, including a one-dollar price you made for a test.