After you check out, your order lives at a tidy URL like /orders/4. The
trouble is the number. The shop looks up the order by its id and shows it to you, but it
never checks the order is actually yours. Change the number and you are reading
someone else’s receipt. This is an insecure direct object reference,
a flavour of broken access control.
New to this? What is IDOR (broken access control)?
An insecure direct object reference, or IDOR, is when an app decides what to show you based on an id in the request (like an order number) without checking that the thing actually belongs to you.
Change the id, see someone else's data. It is a kind of broken access control: the app knows who you are, but forgets to ask whether you are allowed this particular item. The fix is an ownership check on every request.
You need an account for this one. Any customer login works. If you have not got one, the Pick ’n’ Mix injection leaks a table of them for you to crack.
1 · Find it
Log in, add a sweet to your bag, and check out. You land on your order page,
/orders/4 or similar. Note the number. Now try the one below it:
GET /orders/3 If a different order loads, with a name and items you never bought, the shop is handing out orders by number without checking who is asking.
2 · Exploit it
Walk the numbers. Each id is a customer’s order, complete with their email address and what they bought:
/orders/1 -> ada@example.com's order
/orders/2 -> grace@example.com's order
/orders/3 -> alan@example.com's order From one low-privilege account you can read every order in the shop just by counting. That is a customer list, an order history, and a pile of personal data, all leaking through a URL.
Why it matters: IDOR is one of the most common real-world bugs, and scanners routinely miss it because each request looks perfectly valid. It takes understanding of who should be allowed, which is exactly the sort of thing a good tester (or a good tool) has to reason about.
3 · Understand it
The handler requires a login, then fetches the order straight from its id and renders it. The check it is missing is whether the order belongs to the person asking:
app.get('/orders/:id', requireAuth, (req, res) => {
const order = db.prepare('SELECT * FROM orders WHERE id = ?')
.get(req.params.id);
if (!order) return res.status(404).send('no such order');
// ...and it renders the order. No check that it is yours.
res.render('order', { order, ... });
}); Being logged in is not the same as being allowed. Authentication answers “who are you”; this route forgot to ask “are you allowed this one”.
4 · Fix it
Check ownership before you render. The order carries a customer_id. Compare
it to the logged-in user (and let staff through):
const order = getOrder(req.params.id);
if (!order) return res.sendStatus(404);
res.render('order', { order }); const order = getOrder(req.params.id);
if (!order) return res.sendStatus(404);
if (order.customer_id !== req.user.id
&& !req.user.is_admin) {
return res.sendStatus(403);
}
res.render('order', { order });
Now /orders/2 returns a flat 403 for anyone but grace and the staff, and the
receipts stay private.
The general rule: every request for a specific object needs an authorisation check, not just an authentication one. Do it on the server, for every object, every time. Guessable ids make it worse, but random ids are a speed bump, not a fix.