FreeFungi

Challenges FF-003

Gobstopper CWE-639 A01:2021

Someone Else's Order

IDOR on /orders/{id}.

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):

✗ Vulnerable
const order = getOrder(req.params.id);
if (!order) return res.sendStatus(404);
res.render('order', { order });
✓ Patched
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.