When you drop a sweet into your bag, your browser sends the shop a little form. Tucked inside it is the price. The shop takes that number at face value, so if you change it on the way past, you decide what a bag of humbugs costs. This is price tampering, a business-logic flaw where the server trusts a value it should have worked out itself.
New to this? What is price tampering (business logic)?
A business-logic flaw is a bug in the rules of how the app is meant to work, not in a single line of dodgy code. Nothing is technically malformed here; the shop just trusts the wrong side of the conversation.
Price tampering is the classic example: the price of an item is sent from the browser and the server believes it. Because it is valid HTTP doing exactly what it says, scanners tend to miss it. You have to understand what the value means.
You will need to see and change requests. Use a proxy like Burp or ZAP, or your browser’s dev tools. Any customer login works, including the grey-box accounts.
1 · Find it
Add a sweet to your bag with a proxy running, and look at the request to
/cart/add. Alongside the sweet id and quantity is a price:
POST /cart/add
Content-Type: application/x-www-form-urlencoded
sweet_id=12&qty=1&price=150
That price=150 (150 pence, £1.50 of fudge) came from a hidden field on the
page. The shop never re-checks it. That is the whole flaw.
2 · Exploit it
Intercept the request and change the price to whatever you fancy. A penny, say:
sweet_id=12&qty=1&price=1
Let it through, and the fudge sits in your bag at 1p. Check out and the
order is placed and charged at your price. You just bought £1.50 of fudge for a penny, and
the shop thinks that is a perfectly normal sale. Set price=0 and it is free.
Why it matters: this is the bug that quietly costs real shops real money. It rarely throws an error, so it hides in plain sight until the books do not add up.
3 · Understand it
The add-to-bag handler reads the price straight out of the request and stores it, then checkout totals from what it stored:
app.post('/cart/add', (req, res) => {
const price = parseInt(req.body.price, 10); // <- from the browser
cart[sweetId] = { qty, unit_price_pence: price };
// ...checkout later trusts unit_price_pence
}); Only the browser is ever asked what a sweet costs. The server holds the real prices in its own database, but never looks.
4 · Fix it
Never take a price from the client. Look it up on the server from the id, which is the one thing the client is allowed to choose:
const price =
parseInt(req.body.price, 10);
cart[id] = { qty, unit_price_pence: price }; const sweet = getSweet(id); // trusted
cart[id] = {
qty,
unit_price_pence: sweet.price_pence
}; Now the price the shop charges is the price the shop set. Anything the browser sends about money is simply ignored.
The general rule: the client may choose what and how many, never how much. Totals, prices, discounts and permissions are the server’s to decide. Treat every number that affects money as something to recompute, not receive.