SecurityBlog Post

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

4 min read

Updated

Your Checkout Trusts the Price the Browser Sends It
Is your AI-built app exposed? Get a professional vibe coding audit and ship to production with confidence.

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.

Need a second pair of eyes? Explore our code audit services, scope and pricing, and client case studies.

VibeAudits

Security Experts

Worried your vibe-coded app has issues like this?

We run professional code audits for SaaS apps and AI features built with Cursor, Claude, Copilot, Lovable and Replit. We find the security and reliability problems before your customers (or attackers) do, then hand you a fix-ready report.