What is Insecure Direct Object Reference?
Also calledIDORBroken object level authorizationBOLA
Definition
An insecure direct object reference is an access control failure in which an application uses a user-supplied identifier to select a record and does not check that the requesting user is allowed to see that particular record.
What an insecure direct object reference is
The application knows who you are. It checked your session, and the check passed. Then it reads an identifier out of the request, looks up the record with that identifier, and returns it. The step it skipped is the one that asks whether this user is entitled to this record. That is the entire bug, and it is the most reported class in bug bounty programmes for a reason: it survives every framework, every language, and every code review that only looks for injection.
The API-focused naming, broken object level authorization, is more precise and worth preferring in a report, because it names the missing control rather than the visible symptom. The identifier being guessable is not the vulnerability. It only decides how expensive exploitation is.
Why replacing the identifier is not a fix
Swapping a sequential integer for a UUID is the most common response and the least effective one. It raises the cost of enumeration and changes nothing about the missing check. Identifiers leak constantly: in shared links, in exports, in referrer headers, in support tickets, in the very list endpoint that returned them to a user who was allowed to see one of them. A control whose strength depends on an identifier staying secret is not an access control, it is an obscurity budget, and the audit will say so.
How to find it reliably
- Get two accounts. Ideally in two different tenants, otherwise two users in one tenant. Almost every reliable finding in this class comes from a side-by-side comparison rather than from a single trace.
- Record a full session as account A, then replay each request with account B credentials and account A identifiers.
- Read the difference between 403 and 404 carefully. A consistent 404 for both non-existent and unauthorised records is correct behaviour. A 403 for one and a 404 for the other is an enumeration oracle and a separate, lower-severity finding.
- Check the write paths as well as the read paths. Applications frequently authorise a GET properly and forget the PATCH.
- Check nested and indirect references: an identifier inside a JSON body, inside a batch request, inside an export job specification.
What actually fixes it
Scope the query to the actor rather than checking after the fact. A lookup written as "find this record where the owner is the current user" cannot return a record the user is not entitled to, whereas "find this record, then check the owner" is a control someone can forget to add to the next endpoint. Making the scoped form the only convenient way to query, at the data access layer, is what turns this from a per-endpoint discipline into a property of the codebase.
What it looks like in practice
# Two accounts, one identifier. The whole method in four lines.
# Run only against a system you have permission to test.
A_TOKEN="<session token for account A>"
B_TOKEN="<session token for account B, different tenant>"
RECORD="inv_01J8Z2K9QW" # an identifier account A is entitled to
# Baseline: A reading A's own record. Expect 200.
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer $A_TOKEN" \
"https://app.example.com/api/v2/invoices/$RECORD"
# The test: B reading A's record. Expect 404. A 200 is the finding.
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer $B_TOKEN" \
"https://app.example.com/api/v2/invoices/$RECORD"What it is not
The distinction matters more than the definition does, because these are the terms people swap by accident in a report.
5 cards
Tap a card to reveal the answer. Answers are in the page source, so this works with JavaScript disabled.
What is the missing control in an IDOR?
An object-level authorisation check. The application authenticates the user and then selects a record by a user-supplied identifier without asking whether that user is entitled to that record.
Why does switching from sequential IDs to UUIDs not fix an IDOR?
It raises the cost of enumeration and leaves the missing check in place. Identifiers leak through links, exports, referrers and list endpoints, so secrecy is not an access control.
What is the difference between a 403 and a 404 on an unowned record?
A 404 for both unauthorised and non-existent records reveals nothing. A 403 for one and 404 for the other confirms which records exist, which is an enumeration oracle and a separate lower-severity finding.
What is the durable fix at the code level?
Scope the query to the actor: select the record where the owner is the current user, so an unauthorised record cannot be returned, rather than fetching first and checking afterwards.
Why is two accounts the core of the testing method?
The bug only becomes visible in the difference between two sessions. One trace shows an application working correctly; the side-by-side replay shows the missing check.
Where people get this wrong
The reports that get closed are the ones written about the identifier. The reports that get paid are written about the records. Learners spend a lot of energy proving that an identifier is predictable and almost none proving how many records the flaw reaches, which is the number a triager needs in order to assign a severity. Test it with two accounts, state the reachable set, and stop talking about the format of the identifier entirely.
Where you can use this
The G1 gate for this template: the artifacts below exist nowhere else in this combination.
- Lab: IDOR in a multi-tenant invoice APIFind an object-level authorisation failure in a tenanted API and demonstrate cross-tenant access without touching another real customer.
- CTF room: Ledger DriftA small internal accounting application with a self-service export feature and an authorisation model that was written before the tenanting was.
Cyber VK Security Curriculum
Pre-publication content owner
Internal ownership label for the web-security curriculum.
Technically reviewed by Cyber VK Defensive Research
Updated
[ Related ]
- LabIDOR in a multi-tenant invoice APIFind an object-level authorisation failure in a tenanted API and demonstrate cross-tenant access without touching another real customer.
- CTF roomLedger DriftA small internal accounting application with a self-service export feature and an authorisation model that was written before the tenanting was.
- GlossaryServer-Side Request ForgeryServer-side request forgery is a vulnerability in which an attacker causes a server to make an HTTP request to a destination the attacker chooses, using the server as the origin of that request instead of their own machine.
- LabBlind SSRF to cloud metadataProve that a server-side request forgery with no visible response can still reach an internal service, and write the finding so a triager cannot close it as informational.
- GlossaryCross-Site Request ForgeryCross-site request forgery is a vulnerability in which a site an attacker controls causes a victim browser to send a state-changing request to a different site where the victim is already authenticated, using credentials the browser attaches automatically.
- ArticleWhy your SSRF report gets closed as informationalMost blind SSRF reports are closed without a payout, and usually for the same three reasons. Here is what a triager is actually looking for, and how to supply it without touching anything you should not.