Lindsay Edwards

Never trust the client's price

On this page

The first time I wired up a cart, the price came from the browser. It worked. It also meant anyone with the dev tools open could buy a thing for a cent.

That is not a bug you patch later. That is the whole shape of the problem. Every order at the forged price is real money walking out the door, and the browser is the one place you can never lock.

The cart is a question, not an answer#

The cart lived in the browser and stored almost nothing. For each line it held a product id, a size, and a quantity. That is the right amount of information for a cart to carry.

Notice what is missing. There is no price in there.

The checkout endpoint accepts that shape happily. What it will not do is believe a single number the browser sends about money. If the client posts a price, the server ignores it. Every line’s unit amount gets re-derived from the server-side catalog, which is data the client cannot touch.

So the cart is a question. It asks: what would these items cost? The server answers from its own records.

Recompute everything that matters#

Once you accept that the client is untrusted, a checklist falls out of it. On the server I:

  • Reject any product id that is unknown or marked inactive, rather than charging for a ghost.
  • Clamp each quantity to a sane range so nobody orders negative or a million.
  • Cap the number of lines so one request cannot balloon into thousands of catalog lookups.
  • Resolve promotions server-side. A “buy X get Y free” deal is applied by adding a zero-amount line for the free item, decided by the server, not claimed by the client.
  • Decide free shipping from the re-derived subtotal, never from a posted flag.

That last one is the tell. Early on it is tempting to let the browser send freeShipping: true because the browser already knows the subtotal. But the browser knowing something and the browser being trusted to assert it are different things.

Anything the browser can send, an attacker can forge. Treat the cart as an untrusted request for a price, then recompute everything authoritative yourself.

The defender’s habit#

The mental move here is small and it changes everything. You stop asking “did the client send valid data” and start asking “what is the worst thing this field could say, and does my code care.”

If the answer is that my code re-derives the value anyway, the field is harmless. It can lie all it likes. The forged price lands in a variable I never read.

That is the difference between validating input and not depending on it. Validation is nice. Not depending on the client is the thing that actually holds up when someone is trying.

Build the cart to carry identity and intent: what, which size, how many. Let it carry nothing about money.

Then, on the server, treat the whole thing as a request and answer it from data the user cannot reach. Prices, promos, shipping, product validity, all of it recomputed.

I did not get this right the first time. I got it right the first time someone could have exploited it, which is the deadline that matters.

Keep reading