Arkendia Classrooms
Lesson 17 of 28 · 17 min

IDOR: insecure direct object references

Imagine a hotel where every room key opens the door of room 214, but also, if you simply try it, the door of room 215. The lock is real and the key is valid; the problem is that nobody checks whether the room is yours. This is the flaw behind IDOR, one of the most common and most damaging weaknesses in modern applications, and also one of the easiest to understand.

Legal and ethical reminder

Changing an identifier to view another customer’s data is accessing information you are not entitled to see. On a real system without authorisation it is an offence, even if “it was just a number in the URL”. Bug bounty programmes ask researchers to use their own two test accounts to demonstrate the issue and to stop at the first proof. In this lesson you do exactly that, in your lab.

What is an IDOR?

An insecure direct object reference (IDOR) happens when an application uses an identifier supplied by the user (an order number, a user ID, a file name) to fetch a resource, and does not check that the user is allowed to access that particular resource.

It is one expression of a wider category that OWASP ranks at the top of its list: broken access control. Authentication answers “who are you?”. Access control answers “are you allowed to do this, on this object?”. IDOR is a failure of the second question.

A typical example

You log in as a customer and open your latest order. The address bar shows:

GET /orders/1042 HTTP/1.1
Host: localhost:3000
Cookie: session=abc123

The server finds order 1042 and returns it. Your order is number 1042; a different customer’s may be 1041 or 1043. If the server only checks that you are logged in and not that order 1042 belongs to you, then changing the number to 1043 returns someone else’s order. No tool or trick is needed beyond a browser.

Here is the vulnerable logic in pseudocode:

order = database.find_order(id = request.id)
return order            # missing: does order.owner == current_user ?

And the fixed version:

order = database.find_order(id = request.id)
if order is null or order.owner != current_user:
    return "404 Not found"
return order

Where to find IDORs

Look back at your mapping notes from chapter 1. Any identifier is a candidate:

  • Numbers or names in URL paths: /invoices/881, /users/17/profile.
  • Query parameters: ?doc=report-2025.pdf, ?account=1007.
  • Form fields and hidden fields: <input type="hidden" name="user_id" value="17">.
  • JSON request bodies for APIs: {"addressId": 53}.
  • Cookies that store identifiers.
  • File downloads and image references.

APIs are especially prone to this problem, because they expose objects directly and use predictable identifiers. IDORs are also found in write operations: not just reading another user’s order, but modifying or deleting it.

Predictable versus random identifiers

Sequential numbers make exploitation trivial, since an attacker can try neighbouring values. Some applications switch to long random identifiers (UUIDs) and consider the problem solved. It is an improvement, because guessing becomes impractical, but it is not access control. Identifiers leak through shared links, emails, logs, referrers and other pages of the application. If knowing the identifier is enough to access the object, then anyone who sees it gets access. Random identifiers reduce the risk, but the server must still verify ownership.

A method to test for IDOR

The cleanest method uses two test accounts in your lab, call them Alice and Bob.

  1. Create objects with Alice. Make an order, upload a document, write a note. Record their identifiers from the HTTP history.
  2. Log in as Bob in a different browser profile (or use a second session cookie in Burp’s Repeater).
  3. Request Alice’s objects with Bob’s session. In Repeater, take a request for one of Alice’s objects and replace the session cookie with Bob’s. If the server returns the data, you have proof of IDOR, without having to guess a single identifier.
  4. Repeat for each operation. View, edit, delete; each can be protected differently, and developers often secure one and forget the others.
  5. Repeat for each identifier location, including hidden fields and JSON bodies.

This approach produces reliable evidence: a clear “Bob accessed Alice’s data”. It also avoids testing on random identifiers that could belong to real third parties in a production system.

What responses tell you

Response when Bob requests Alice’s objectInterpretation
200 with Alice’s dataIDOR confirmed
403 ForbiddenAccess control works; note that the response confirms the object exists
404 Not foundOften the best behaviour, as it does not reveal that the object exists
200 with an empty or generic pageProbably handled; check the body carefully

Compare response sizes and contents, not only status codes. Some applications return 200 with an error message inside the page.

Variants

  • Function-level reference. An admin page is “hidden” but reachable by anyone who types its path. We cover this in the next lesson.
  • Mass assignment. A request to update your profile accepts extra fields that you were not supposed to set, such as "owner_id" or "role". The server binds all fields automatically to the object.
  • Indirect IDOR. The ID is not in the direct request, but in a related step: for example, a “download my invoice” button that sends the invoice ID to an export service.
  • File path references. A download feature that takes a file name and does not restrict which files can be requested.

The impact

The impact depends on what the objects contain. Order histories, medical records, invoices, private messages, address books and support tickets are all examples that can have serious privacy consequences, including data protection implications under rules such as the GDPR. IDORs on write operations can allow modification or deletion of other people’s data. Because the vulnerability is so simple to exploit and difficult for generic scanners to detect (they do not know which objects belong to whom), it is a staple of bug bounty reports.

Prevention

  • Check authorisation on every request, on the server. For each object, verify that the current user may perform this action on it. Put the check in a central place (a policy layer or a shared function), rather than repeating it in each endpoint where it can be forgotten.
  • Deny by default. If no rule allows the action, refuse it.
  • Derive the owner from the session, not from a parameter. For “my profile”, the server should use the authenticated user’s ID, not an ID sent by the client.
  • Scope queries to the user. Instead of finding an order by id and then checking, look it up with both conditions: the id and the owner.
  • Use unpredictable identifiers as a complementary measure, never as the only one.
  • Test it. Automated tests that call every endpoint as the wrong user catch regressions.
-- Scoped lookup: the owner condition is part of the query
SELECT * FROM orders WHERE id = :id AND user_id = :current_user_id;

Tip

Burp offers extensions that automate replaying requests with a different session. They are helpful once you understand the principle. But the manual two-account approach teaches you what to look at, and the logic is what really finds bugs.

Your turn: look for IDOR in your lab

With two accounts in your lab application (or in a Juice Shop basket / Academy IDOR lab), list three features that use an identifier, and test each one with the Alice-and-Bob method. In your notebook, record for each: location of the identifier, operation tested (view, modify, delete), and result.

See a solution

An example table:

FeatureIdentifier locationOperationResult
View an orderURL pathViewBob receives Alice’s order: vulnerable
Edit delivery addressJSON body, addressIdModify403 for Bob: protected
Download invoiceQuery parameterView200 with Alice’s invoice: vulnerable

Recommendation for the two findings: verify ownership on the server at every request and scope the database lookups to the current user. Notice how the edit feature was protected but the view was not — a very common inconsistency.

Key takeaways

  • An IDOR occurs when the application uses a user-supplied identifier without checking that the user may access that object.
  • It is a form of broken access control, listed at the top of OWASP’s risk categories.
  • Test with two accounts, replaying one user’s requests with the other user’s session, for view, edit and delete, in every identifier location.
  • Random identifiers help but are not a substitute for server-side authorisation checks.
  • Fix with centralised, deny-by-default checks, owner derived from the session and queries scoped to the user.

IDOR gives access to someone else’s data at the same level of privilege. The next lesson looks at moving up the ladder.

Advertisement
Log in to save your progress. Log in