EXERCISE 2 — Fixing an IDOR with a server-side ownership check ============================================================== THE VULNERABLE ENDPOINT: // returns ANY invoice by id, no ownership check app.get("/api/invoices/:id", requireLogin, async (req, res) => { const invoice = await db.invoices.findById(req.params.id); res.json(invoice); }); THE ATTACK: - The endpoint checks the user is LOGGED IN (requireLogin) but NOT that the invoice belongs to them. So an authenticated attacker simply changes the id in the URL: GET /api/invoices/1043 -> their own invoice (fine) GET /api/invoices/1044 -> someone else's invoice (leaked) - By WALKING THE IDs (1, 2, 3, ... or scripting it), they can dump EVERY invoice in the system — names, amounts, addresses. This is IDOR / horizontal privilege escalation, and it's trivial: no tools, just incrementing a number. (Authentication is present; AUTHORIZATION is missing.) THE SERVER-SIDE OWNERSHIP-CHECK FIX: app.get("/api/invoices/:id", requireLogin, async (req, res) => { const invoice = await db.invoices.findById(req.params.id); // 1) exists? 2) does it belong to the authenticated user? if (!invoice || invoice.userId !== req.user.id) { return res.status(404).send("Not found"); // 404, not 403 — see note } res.json(invoice); }); Key points: - The check uses req.user.id from the AUTHENTICATED SESSION (server-trusted), compared against the invoice's owner — NOT anything the client supplied. - It runs on EVERY request to this object. The decision is "does THIS user have rights to THIS specific record?", not merely "is the user logged in?" - Prefer a query scoped to the owner so you can't forget the check: const invoice = await db.invoices.findOne({ id: req.params.id, userId: req.user.id }); if (!invoice) return res.status(404).send("Not found"); This makes the ownership constraint part of the data access itself. - Returning 404 (not 403) for someone else's id avoids confirming the record EXISTS (reduces enumeration/info leak). Either is far better than leaking the data. - Centralize this as a reusable authorization helper/middleware so every object endpoint enforces ownership consistently (don't re-implement ad hoc). WHY RANDOM UUIDs HELP BUT DON'T REPLACE THE CHECK: - Sequential integer ids are trivially ENUMERABLE (1044, 1045, ...). Switching to random UUIDs (e.g. 9f8c1a...e2) makes ids HARD TO GUESS, so an attacker can't simply walk them — a useful DEFENCE IN DEPTH that shrinks the attack surface. - BUT it is NOT access control. If the attacker OBTAINS a valid UUID — from a shared link, a referer header, a log, an email, another endpoint, the browser history, or an API that returns other users' ids — the vulnerable endpoint STILL hands over the record, because there's no ownership check. Unguessable is not the same as unauthorized. - So: use UUIDs to make enumeration hard, but ALWAYS enforce the server-side ownership check as the real defence. Obscurity supplements; it never substitutes. ONE-LINE TAKEAWAY: Fix IDOR by verifying, server-side on every request, that the authenticated user owns the specific object (ideally by scoping the query to req.user.id); random UUIDs are a helpful extra layer, not the fix.