Skip to content
العربية

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.

Estimated time
20 min
Level
Beginner
Your progress in this track0%

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:

your request
GET /invoice?id=1050

The server looks up invoice 1050 and shows it. But what happens if you ask for a different number?

someone else's request
GET /invoice?id=1000

If 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

safe.js
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.

Review questionAn app switched from /record?id=1050 to /record?ref=MTA1MA== (Base64 of 1050) and still shows any record. Is it safe now?

Labs

  • A record with a sequential number

    EasyBrowser lab

    Read the leader’s record and get his stash code, even though it is not your record.

  • A reference that looks random

    MediumBrowser lab

    Read the leader’s record again, even though the reference is no longer a simple number.

  • A forgotten admin back door

    HardBrowser lab

    Read the leader’s record once more, even though his reference is an unguessable UUID.