Your Checkout Trusts the Price the Browser Sends It
AI-built checkout flows often let the browser tell the server what the price is. Anyone with dev tools open can change that number before they pay.
By Abhay Mittal
Updated

A checkout is vulnerable to price tampering when its server accepts a browser-supplied amount instead of looking up trusted pricing.
AI coding tools build checkout flows fast. They wire up a cart, a total, and a call to Stripe or PayPal in minutes. But they often skip one check: where does the price actually come from? If the browser sends the price and the server just accepts it, anyone can open dev tools and pay one cent for a $200 item.
This happens because the AI writes the checkout form first. The form already knows the price, since it renders it on the page. So when it builds the API call, it just sends that price along with the request. It feels natural. It also means the price now lives in a variable the user controls.
What a vulnerable checkout looks like
This simplified example shows the trust boundary. Authentication, order creation, and payment confirmation need their own checks.
// Broken: server trusts the price from the client
app.post("/api/checkout", async (req, res) => {
const { productId, price, quantity } = req.body;
const amount = price * quantity;
await stripe.paymentIntents.create({
amount: amount * 100,
currency: "usd",
});
res.json({ success: true });
});A user can open the network tab, edit the request, change price to 0.01, and resend it. The server has no way to know that number is fake. It just multiplies and charges.
Validate price and quantity on the server
Accept a product ID and a requested quantity, validate both, and look up pricing on the server. Store the amount in integer minor units for the currency you charge. For Stripe Checkout, create the session on your server using trusted Price IDs or trusted server-generated pricing.
// Fixed: server looks up the real price
app.post("/api/checkout", async (req, res) => {
const { productId, quantity } = req.body;
if (typeof productId !== "string" || !Number.isInteger(quantity) || quantity < 1 || quantity > 50) {
return res.status(400).json({ error: "Invalid product or quantity" });
}
const product = await db.product.findUnique({
where: { id: productId },
});
if (!product) {
return res.status(404).json({ error: "Product not found" });
}
// priceInCents is trusted integer pricing from the server's catalogue.
const amount = product.priceInCents * quantity;
if (!Number.isSafeInteger(amount) || amount <= 0) {
return res.status(400).json({ error: "Invalid amount" });
}
await stripe.paymentIntents.create({
amount,
currency: "usd",
});
res.json({ success: true });
});Notice the request body only sends productId and quantity now. The price never comes from the client at all. The server owns the number that gets charged, not the browser.
This same bug shows up with discount codes and shipping costs too. If your server reads a discount or shippingCost field straight from the request body, someone can zero those out the same way. Every dollar amount in a checkout flow needs to come from your own data, calculated on your own server.
Validate quantity as an integer within your business limits. Reject missing, fractional, negative, and oversized values. A negative payment amount is not a refund instruction; refunds use separate provider APIs and require their own authorization.
Test your checkout in payment test mode
Using your own test accounts, resend the checkout request with a changed price, a forged discount, and invalid quantities. Confirm the provider receives the trusted server-side amount, and invalid requests are rejected. A price in the browser request alone does not prove a vulnerability: the server may already ignore it or validate it correctly.
Then check that creating a payment does not fulfill an order. Grant access only after verifying the relevant payment state through a signed webhook, with duplicate delivery handled safely.
Want a second pair of eyes on your checkout flow? Book an audit call to scope a professional review.
VibeAudits audits apps built with Cursor, Lovable, Bolt, Claude Code, Replit, and other AI tools.
Keep reviewing your app
Practical checks for the parts of an AI-built app that handle real users and money.
Free vibe code security check
Answer 15 questions to score your app's launch risk and see what to fix first.
Verify Stripe webhook signatures
Check that payment events are verified before your app grants paid access.
Vibe-coded app security checklist
Work through 25 checks before accepting real users, payments, and private data.
Need a second pair of eyes? Explore our code audit services, scope and pricing, and client case studies.