← Todas las noticias
security5 min de lectura

When the customer sets their own price

Por Seaworthy · 3 de julio de 2026

Red padlock resting on a black computer keyboard

Photo by FlyD on Unsplash

The bug that ships in almost every vibe-coded checkout

I've seen this bug in more codebases than I can count, and it never stops surprising me. Someone builds a checkout flow with AI assistance, the cart works perfectly in their browser, all the numbers look right, and they ship it. The bug is invisible from the outside until someone decides to open the browser console.

Here's how it happens. The AI writes a POST /checkout handler. The handler accepts a JSON body from the client. That body includes the items, the quantities, maybe a discount code. And, very often, it also includes the price.

// Vulnerable pattern: trusting price from the client
app.post('/checkout', async (req, res) => {
  const { items, total } = req.body;
  // items = [{ productId: 'prod_123', quantity: 2, price: 29.99 }]

  const order = await db.orders.create({
    items,
    total,
    userId: req.user.id,
  });

  await stripe.paymentIntents.create({
    amount: Math.round(total * 100), // total came from the browser
    currency: 'gbp',
  });

  res.json({ orderId: order.id });
});

The product costs £29.99. But the user sends "price": 0.01 in the request body. The server accepts it. The order is created. Stripe charges one penny. You've just sold your product for £0.01.

This is not a sophisticated attack. It requires no special tooling and no knowledge of your infrastructure. curl is enough.

Why AI assistants keep generating this pattern

The short answer: the AI is writing plausible-looking code that mirrors the shape of the data flowing through the app. The client builds a cart, so the cart data naturally ends up in the request. The server receives it, stores it, charges it. From a pure code-flow perspective, it looks coherent.

The AI isn't reasoning about the trust boundary between the browser and the server. It's producing code that compiles and looks like the checkout handlers it was trained on. A lot of real production code makes this same mistake, so the pattern gets reinforced.

There's also a subtler version. The AI correctly looks up the price from the database for the total, but still accepts the quantity from the client without bounds checking. Send quantity: -1 and you might get a negative charge. Send quantity: 999999 and you'll overflow whatever integer column you're using. The point is that the attack surface is wider than just the price field.

The fix: never trust the client for financial values

The rule is simple. On the server, look up the price. Always. The client sends you product IDs and quantities, nothing more.

// Safer pattern: look up prices server-side
app.post('/checkout', async (req, res) => {
  const { items } = req.body;
  // items = [{ productId: 'prod_123', quantity: 2 }]
  // Note: no price field expected or accepted from client

  // Validate quantities before doing anything else
  for (const item of items) {
    const qty = parseInt(item.quantity, 10);
    if (!Number.isInteger(qty) || qty < 1 || qty > 100) {
      return res.status(400).json({ error: 'Invalid quantity' });
    }
  }

  // Fetch prices from the database, never from the request
  const productIds = items.map(i => i.productId);
  const products = await db.products.findMany({
    where: { id: { in: productIds }, active: true },
  });

  const lineItems = items.map(item => {
    const product = products.find(p => p.id === item.productId);
    if (!product) throw new Error(`Unknown product: ${item.productId}`);
    return {
      productId: item.productId,
      quantity: parseInt(item.quantity, 10),
      unitPrice: product.price, // from the database, not the request
      lineTotal: product.price * parseInt(item.quantity, 10),
    };
  });

  const total = lineItems.reduce((sum, li) => sum + li.lineTotal, 0);

  const order = await db.orders.create({
    lineItems,
    total,
    userId: req.user.id,
  });

  await stripe.paymentIntents.create({
    amount: Math.round(total * 100),
    currency: 'gbp',
    metadata: { orderId: order.id },
  });

  res.json({ orderId: order.id });
});

Notice what changed. The client no longer sends a price. The server looks up the price for every product by ID, validates that those products exist and are active, and computes the total itself. The Stripe charge is derived entirely from server-side data.

The quantity validation matters too. Cast it explicitly to an integer, clamp it, and reject anything outside a sensible range. Don't leave it as a raw value from req.body.

This applies to discount codes too

The same logic applies to discounts. If the client sends a discount percentage and the server applies it without verification, you have the same category of problem. The client claims a 90% discount exists. Does it? The server needs to verify that claim independently, look up the discount's actual value from the database, and check whether it applies to the current user and order.

Never accept computed financial values from untrusted sources. Accept identifiers and let the server do the arithmetic.

What to audit in your own codebase

Run a search across your API handlers for any route that creates orders, invoices, or payment intents. Look at every field you're reading from req.body. If any of those fields are prices, totals, or discounts without server-side confirmation, you have a problem worth fixing before someone finds it.

Quantity fields deserve the same attention. Check that you're parsing them to integers, not treating them as floats, and rejecting values outside an expected range. A negative quantity is not a valid input, and your code should say so explicitly.

The pattern is common enough that static analysis catches it. Seaworthy flags this class of trust-boundary mistake as part of its server-side validation checks, alongside the other issues that quietly ship in AI-assisted codebases.

Este artículo fue generado por IA y resume fuentes públicamente disponibles.

De los creadores de Seaworthy

Seaworthy analiza tu repositorio en busca de los problemas que se tratan en artículos como este.

Brechas de seguridad, secretos expuestos y configuraciones erróneas detectados antes de desplegar. Gratis, sin cuenta necesaria.

$ npx seaworthycode