What is IDOR?
Most breaches you imagine need a clever exploit. This one needs a number in a web address and an app that never checked whether the number was yours to ask for.
IDOR stands for Insecure Direct Object Reference. The name is clumsy. The idea is not. An application lets you point at something, a record, a file, an invoice, by its reference, usually an id in the address or the request. Then it hands the thing over without checking that it belongs to you.
Here is what that looks like at full scale. In 2019, the title insurance company First American Financial left about 885 million documents reachable on its website: mortgage paperwork, Social Security numbers, bank account numbers, images of driver’s licenses. There was no break-in. The documents sat at addresses like .../document/000000075. Change the number, see someone else’s file. Anyone who had ever been emailed a link to a single document could walk forward and backward through hundreds of millions of them by editing one digit. That is IDOR.
How it happens
Picture a page that shows your profile:
https://example.com/profile?id=123
The code behind it reads the id, fetches that record, and returns it:
record = db.get_profile(request.args["id"])
return render(record)
Nothing checks who is asking. So the attacker changes the number:
https://example.com/profile?id=124 # someone else
https://example.com/profile?id=1 # often the first account created, often an admin
The modern version lives in mobile apps and APIs, where this is now the most common serious flaw there is:
GET /api/v1/accounts/5213/statements
The app trusts the account number in the path. Swap it for 5214 and you read another customer’s statements. OWASP tracks this as its own category, Broken Object Level Authorization, and it has sat at number one on the OWASP API Security Top 10 since 2019, because APIs made it so easy to get wrong.
The root cause never changes. The application trusts the request to tell it what the user is allowed to see. But the request is written by the attacker. Which record it names is not something the app may take on faith.
What an attacker does with it
- Read records that are not theirs: statements, messages, medical results, support tickets.
- Change or delete objects they do not own.
POST /orders/889/cancelwith someone else’s order number cancels someone else’s order. - Act as another user, by aiming write actions at ids that belong to other accounts.
- Climb. An id that points at an admin object turns a small hole into full control.
The fix
One check does almost all of the work. Verify, on the server, on every request, that the caller is allowed to touch this exact object. Take who the user is from the session, never from the request.
# The id says WHICH object. The session says WHO is asking.
# The server, not the user, decides whether the two may meet.
record = db.get_invoice(request_id)
if record is None or record.owner_id != session.user_id:
raise Http403("Not your record")
return record
That is the whole game. Everything below is a second layer, not a substitute.
- Hard to guess is not the same as protected. Replacing
1, 2, 3with random UUIDs slows a guesser down. It authorizes no one. An attacker who gets a valid UUID from a shared link, a log file or a browser referrer is right back in. Obscurity is not a permission check. - Put the check in one place. An ownership rule copied into forty request handlers is a rule missing from the forty-first. Enforce it in a layer every request passes through, so a new endpoint is covered by default, not by memory.
- Watch for the pattern. One account pulling records in numeric order is not a person. It is a script. Alarm on it.
Why it survives code review and testing
IDOR is invisible in a demo. Log in as yourself and every screen shows your own data, so everything looks correct. The flaw only appears when someone asks for an id that is not theirs, and no one sees that unless they think to try it. Automated scanners miss it for the same reason: the request is perfectly well formed. Only a tester who knows which id belongs to which user can tell that a valid-looking response was the wrong answer.
The question underneath
IDOR is not a coding trick gone wrong. It is a question the application forgot to ask. Every request that names an object is really two questions bolted together: which object, and who wants it. Most apps answer the first and assume the second. The assumption is the vulnerability.
So when you review your own systems, do not only check that each endpoint returns the right shape of data. Put someone else’s id in the request and watch what comes back. The endpoints that hand it over without a second thought are the ones that cost First American 885 million records. Before your app returns an object, it should be able to answer one question in its own code, not in your hope: is this caller allowed to see this exact thing?