IDOR (Insecure Direct Object Reference)
How changing an identifier in a request exposes data that is not yours, because the server does not check who owns it — and why hiding the identifier is not the fix.
- Track
- Web security
- Estimated time
- 20 min
- Level
- Beginner
Before this lesson
IDOR happens when an application lets you reach an object — a record, a file, an order — by an identifier you send, and never checks that the object is yours. Change the identifier and you reach someone else’s object. It is a kind of broken access control, and it is everywhere, because almost every page is “show me object N”.
What is the vulnerability?
You open your own invoice:
GET /invoice?id=1050The server looks up invoice 1050 and shows it. But what happens if you ask for a different number?
GET /invoice?id=1000If the server returns invoice 1000 without checking that it belongs to you, you are now reading another person’s invoice. The identifier is a direct reference to the object, and using it is insecure because nothing authorizes you for that specific object.
Why does it happen?
Developers check authentication (are you logged in?) and forget authorization at the object level (may you see this object?). The lookup db.record(id) happily returns any record; the missing line is “…and is it yours?”. The identifier comes from the user, so the user can change it.
Hiding the identifier is not the fix
A common wrong fix is to make the identifier hard to guess — Base64-encode it, or hash it. That is Relying on a detail being secret or hard to find, instead of on an actual access check. It slows an attacker down; it does not stop them.. Base64 is not encryption — anyone decodes it and re-encodes another id. Even a random, unguessable id fails if another part of the app leaks it (a list page, an admin route left open). The fix is always the same: authorize the object on the server.
How to prevent it
app.get('/invoice', (req, res) => {
if (!req.session.user) return res.status(401).end(); // authentication
const invoice = db.invoice(req.query.id);
if (!invoice || invoice.ownerId !== req.session.user) { // authorization (object level)
return res.status(403).end();
}
res.json(invoice);
});Check ownership on every object request, verify roles on every privileged function, and prefer references scoped to the session (look up “the current user’s invoices” rather than “any invoice by id”) so there is no foreign id to pass in the first place. A random UUID is a reasonable extra, but it is never a substitute for the check.
Labs
A record with a sequential number
SolvedEasyBrowser lab
Read the leader’s record and get his stash code, even though it is not your record.
Enter the labLockedA reference that looks random
SolvedMediumBrowser lab
Read the leader’s record again, even though the reference is no longer a simple number.
Enter the labLockedA forgotten admin back door
SolvedHardBrowser lab
Read the leader’s record once more, even though his reference is an unguessable UUID.
Enter the labLocked
