IDOR Explained: Insecure Direct Object References
What IDOR is, how missing authorization on object IDs leaks or changes other users' data, why guessable IDs are not the real cause, and how to defend.
Some of the most damaging web bugs are also the simplest to trigger. Insecure direct object reference, universally shortened to IDOR, is the standout example. Exploiting one can be as easy as editing a number in a URL. No special tools, no clever payload, just asking the server for a record that belongs to someone else and getting it.
IDOR is a kind of broken access control. The application exposes a reference to an internal object, an account, an invoice, a message, a file, and lets the user hand that reference back to fetch the object. The bug is that the application fetches whatever reference it is given without first checking whether this user is allowed to have it.
A worked example
Picture an app where you view your own invoice at a URL like this:
https://shop.example/invoices/1043
The number 1043 is a direct object reference. It points straight at a row in the invoices table. You are allowed to see it because it is yours. Now you change the number:
https://shop.example/invoices/1044
If the application returns invoice 1044, and 1044 belongs to a different customer, that is IDOR. The server looked up the record by the ID in the URL and sent it back. It never asked the question that mattered: does the person making this request own invoice 1044?
That missing question is the entire vulnerability. Every other detail, how the ID is formatted, where it appears, whether it is a number or a string, is secondary.
It helps to name what a correct version would do. Before returning invoice 1044, the server should establish who is making the request, look up who owns invoice 1044, and confirm those are the same party or that the requester holds a role that grants access. Only then does it send the data. The vulnerable version skips straight from the ID to the record, and the skipped step is the whole of the security. Notice that this has nothing to do with authentication. The attacker in the example is logged in as a perfectly valid user. Authentication proves who you are; authorization decides what you may touch. IDOR is a failure of the second, not the first, which is why a fully logged-in, entirely legitimate account can still walk off with another user's data.
Why guessable IDs are a red herring
A common first reaction is to blame the sequential number. If the IDs were random and long, an attacker could not guess the next one, so surely the bug goes away. It does not. It only gets quieter.
Unpredictable identifiers make casual discovery harder, and that has some value. They do not make the reference secret. Object IDs leak constantly through browser history, server logs, referrer headers, analytics tools, shared links, and support tickets. An attacker who obtains one valid identifier for another user, by any of these routes, walks straight through an app that relies on unguessability. A random ID is a speed bump, and it is being mistaken for a locked door.
Switching from sequential IDs to random ones changes how an attacker discovers a reference, not whether the app will honor it. If the server hands over any object whose ID is presented, the only real fix is to check ownership on every request. Treat unguessable IDs as a small hardening measure layered on top of a real access check, never as the check itself.
Read-side and write-side
IDOR is often described as a data-leak bug, and reading other users' records is the familiar version. The write side can be worse.
| Variant | What the attacker does | Typical impact |
|---|---|---|
| Read-side IDOR | Fetches another user's object by substituting its ID | Confidential data disclosure |
| Write-side IDOR | Updates another user's object via its ID | Tampering with records the attacker does not own |
| Delete-side IDOR | Removes another user's object | Destruction or denial of service |
| Function-level gaps | Reaches an action the role should not have | Privilege escalation |
If an endpoint that edits a profile, changes an email, or cancels an order trusts an ID in the request without checking ownership, an attacker can act on records that are not theirs. Reviewing only the read paths and assuming the write paths are safe is a frequent and costly mistake.
Where the IDs live
An attacker will look for object references everywhere the app accepts input, not only in the visible URL.
- Path segments, like the invoice number in the example.
- Query parameters, such as an
idoraccountvalue. - Request body fields in JSON or form submissions, especially on APIs.
- Headers and cookies that name a tenant, an organization, or a user.
- Indirect references in file names, export links, and download tokens.
Any of these is a candidate for substitution. A thorough review walks every endpoint and asks, for each object reference it accepts, whether the code confirms the caller is entitled to that specific object. This maps to the Broken Access Control category at the top of the OWASP Top 10.
How IDOR gets missed
If the fix is simply an ownership check, it is fair to ask why IDOR remains one of the most reported bugs in the wild. The answer is that the check is easy to write once and easy to forget everywhere else.
Applications grow endpoint by endpoint. A developer adds a new route to fetch a resource, focuses on getting the data back correctly, and the authorization step, being invisible in the response, is the part most easily left out. The endpoint works in every test, because the tester is logged in as the owner of the object they are fetching. Nothing looks wrong until someone requests an object they do not own, and by then the code has shipped.
The problem compounds on APIs. A single application may expose the same object through several endpoints: a web route, a mobile API, an admin panel, a bulk export. Each is a separate place the ownership check must be repeated, and it only takes one that forgot. Attackers know this and specifically hunt the less-trodden endpoints, the export links and the mobile backends, where the check is most often absent.
Framework defaults do not help here. Unlike injection, where prepared statements make the safe path the easy path, most frameworks do not enforce object-level authorization for you. The safe behavior is something the developer must add on purpose, on every route, which is exactly the kind of repetitive discipline humans are bad at sustaining. That is why centralizing the check matters so much, and why testing with a second account belongs in the pipeline rather than in someone's memory.
How to actually defend against IDOR
The defense is conceptually small and operationally demanding: check authorization on every request, for every object, on both reads and writes.
- Enforce a per-request ownership check. Before returning or modifying an object, verify that the authenticated user is permitted to act on that exact object. Scope the lookup to the user where you can, for example querying for an invoice that belongs to the current user rather than fetching by ID and checking afterward.
- Deny by default. Make the absence of an explicit permission mean no access. An endpoint that forgets to check should fail closed, not open.
- Centralize the authorization logic. Put the check in a shared layer, middleware, a policy module, a query scope, so individual endpoints cannot silently skip it. Scattered ad hoc checks are how one forgotten endpoint becomes a breach.
- Cover writes and deletes, not just reads. Apply the same ownership check to every state-changing action. These are the highest-impact paths.
- Use indirect references where it fits. Mapping the user's view to per-session identifiers, so a user only ever sees references valid for them, adds a layer. It supplements the authorization check; it does not replace it.
- Test with two accounts. The reliable way to find IDOR is to take a valid request from one user and replay it as another. Build this into review and automated testing so regressions surface early.
For every endpoint that touches an object by ID, answer one question in the code itself: is the current user allowed to act on this specific object. If that check is not present and enforced server-side, the endpoint is vulnerable no matter how the ID is formatted. Make the check a required, centralized step and IDOR stops being possible to forget.
Detection signals
IDOR is found more by testing than by watching traffic, and once you know the shape it also leaves traces in logs. The concrete signals below are where teams surface it.
- Sequential identifier walking. One authenticated session requesting a run of object IDs it has no prior relationship with, especially incrementing or decrementing values, is the classic probe. A user fetching invoices, messages, or profiles across a range of IDs in quick succession rarely reflects normal use.
- Access to objects with no prior link to the account. Telemetry that ties each object access to the owning account can flag a request where the requester and the object owner differ. A successful IDOR is exactly this: a valid session touching an object it does not own.
- A spike in authorization-relevant errors, or the absence of them. On an app with proper checks, cross-account requests produce a wave of access-denied responses, which is itself a signal of probing. On a vulnerable app the same requests succeed silently, so the tell shifts to unusual access patterns rather than error bursts.
- Enumeration across less-trodden endpoints. Requests hitting export links, mobile API routes, and admin-style endpoints with substituted identifiers, particularly from a session that does not normally use them, point at an attacker hunting the endpoints where the check is most often missing.
- Object references appearing from unexpected sources. An identifier valid for one user showing up in another user's requests can indicate a reference that leaked through a shared link, a referrer header, or a log, followed by an attacker replaying it.
The most reliable discovery method remains active testing rather than passive monitoring, because a vulnerable endpoint answers a cross-account request cleanly and leaves little to alert on. That is the argument for building two-account replay into the test pipeline, covered in the defense list below.
A step-by-step test for IDOR
Finding IDOR is a mechanical process that any review can follow. The steps below turn the two-account idea into a repeatable check.
- Create two separate accounts. Call them the victim and the attacker. Give the victim some data: an invoice, a message, a profile, a file.
- Capture a request as the victim. Log in as the victim and perform an action that touches one of their objects, then record the full request, including the object identifier, the path, and any body fields.
- Replay it as the attacker. Log in as the attacker, take the victim's captured request, and send it using the attacker's session credentials while keeping the victim's object identifier.
- Read the result carefully. If the attacker's session returns the victim's object, the endpoint is vulnerable. If it returns an access-denied response or the attacker's own data, the check is holding for that path.
- Repeat across every variant of the object. Try the read route, the write route, the delete route, the export link, and the mobile or admin API for the same object. Each is a separate check, and a single passing route does not vouch for the others.
This procedure scales into automation. Recording a suite of authenticated requests from one account and replaying them under another, then asserting that each is denied, turns IDOR regression testing into something continuous rather than a one-time manual pass.
How IDOR compares to related access-control bugs
IDOR sits inside the broad category of broken access control, and distinguishing it from its neighbors sharpens where each fix belongs.
| Bug | What is missing | Attacker action | Where the fix lives |
|---|---|---|---|
| IDOR (object-level) | A per-object ownership check | Substitutes another user's object identifier | Verify the caller owns the specific object, on every request |
| Function-level access control gap | A per-action role check | Reaches an action their role should not have | Enforce role requirements on every privileged endpoint |
| Privilege escalation | Correct enforcement of role boundaries | Gains rights beyond their assigned role | Constrain roles and validate every elevation |
| Mass assignment | A field-level write allowlist | Sets object fields they should not control | Bind only permitted fields from input |
The closest relatives are function-level gaps and mass assignment. A function-level gap is about reaching an action the role should not perform, such as an ordinary user calling an admin-only route, while IDOR is about reaching a specific object the user should not touch. Mass assignment overlaps on the write side: where IDOR lets an attacker act on an object they do not own, mass assignment lets an attacker set fields on an object they may legitimately touch, such as flipping their own role field during a profile update. The two often travel together on the same endpoints, and both come down to the server trusting input it should have constrained.
Common misconceptions
Random identifiers fix IDOR. Unpredictable identifiers make casual discovery harder and change nothing about whether the server honors a presented reference. Object IDs leak through history, logs, referrers, and shared links, so an attacker who obtains one walks straight through an app that relies on unguessability. The fix is an ownership check, and randomness is a small hardening measure on top of it.
Authentication covers it. IDOR is exploited by a fully authenticated, entirely legitimate user acting on objects that are not theirs. Authentication proves identity. Authorization decides what that identity may touch, and IDOR is a failure of the second, which no amount of login strength addresses.
Only read access is at risk. The write and delete sides are frequently higher impact, because they let an attacker modify or destroy records they do not own. A review that checks read paths and assumes the write paths are safe leaves the more damaging routes open.
The framework handles it. Unlike injection, where safe defaults make the secure path the easy one, most frameworks do not enforce object-level authorization automatically. The check is something a developer must add deliberately on every route, which is precisely why it is so often forgotten on the less-trodden endpoints.
Frequently asked questions
What does IDOR stand for and what is it? IDOR stands for insecure direct object reference. It is a broken access control bug where an application uses a user-supplied identifier to fetch or change an object without first confirming the requester is allowed to act on that specific object. The classic form is editing an identifier in a URL and receiving another user's data.
Is IDOR the same as broken access control? IDOR is one specific type of broken access control, the category that tops the OWASP Top 10. Broken access control is the broad family of failures to enforce what a user may do or reach, and IDOR is the object-level case where a missing ownership check lets a user touch objects that are not theirs.
Why are unguessable IDs not a real fix? Because they change how an attacker discovers a reference without changing whether the server honors it. Identifiers leak constantly through browser history, server logs, referrer headers, analytics, and shared links. An attacker who obtains one valid identifier by any of these routes walks straight through an app that trusts unguessability, so the reference has to be checked on every request. Making it hard to guess does not substitute for that check.
Is write-side IDOR worse than read-side? Often, yes. Read-side IDOR discloses data, which is serious. Write-side and delete-side IDOR let an attacker modify or destroy records they do not own, which can mean tampering, fraud, or denial of service. Apply the ownership check to every state-changing action, since those are the highest-impact paths.
How do I test my own application for IDOR? Create two accounts, capture a request that touches an object as the first user, then replay it with the second user's session while keeping the first user's object identifier. If the second account receives the first account's object, the endpoint is vulnerable. Repeat across read, write, delete, export, and API variants of the same object.
Where do developers most often forget the check? On the less-trodden endpoints: export links, bulk downloads, mobile API backends, and admin panels that expose the same object through a different route. A single object is frequently reachable through several endpoints, and it only takes one that omits the ownership check, which is why centralizing the check matters.
Does centralizing authorization actually help? Yes. Putting the ownership check in shared middleware, a policy module, or a query scope means individual endpoints cannot silently skip it. Scattered ad hoc checks are how one forgotten route becomes a breach, so a central, deny-by-default layer that every request passes through is the durable structural fix.
How IDOR stays common
IDOR has been a top-reported class for as long as web applications have exposed object identifiers, and its persistence has a structural explanation. The industry solved injection largely by making the safe path the default: parameterized queries and templating engines mean a developer following the ordinary pattern gets safety for free. Object-level authorization never got that treatment. Most frameworks route a request to a handler and hand it the identifier without any built-in notion of whether the caller owns the thing that identifier names. The safe behavior is an explicit step the developer must remember on every route.
The growth of APIs widened the gap. A single object is now commonly exposed through a web route, a mobile backend, a public API, an admin panel, and a bulk export, each written at a different time by different people. The ownership check has to be present on all of them, and attackers specifically probe the ones most likely to have missed it. Modern practice has responded by pushing the check into shared authorization layers and policy engines, and by treating two-account replay testing as a standard part of the pipeline. The bug remains common because the discipline it requires is repetitive and invisible in a passing test, which is exactly the kind of work that slips.
IDOR endures because the exploit is trivial and the defense is a discipline. There is no exotic payload to filter and no library that patches it for you. There is only the question of whether your code checks, on every single request, that this user may touch this object. Make that check mandatory and central, apply it to reads and writes alike, and treat unguessable IDs as the minor hardening they are. To see how access-control flaws move from disclosure to real-world use, cross-reference our Exploit Intelligence dashboard.
Related guides
Sources & further reading
Related guides
- Clickjacking Explained: UI Redress Attacks & Frame Defenses
How clickjacking hijacks a user's clicks with transparent iframes, what an attacker can trigger, and how to block framing with headers and CSP.
- Command Injection: Shell Metacharacters & Safe APIs
How OS command injection breaks out of an intended command via shell metacharacters, and why argument-array APIs beat string concatenation.
- CSRF Explained: SameSite Cookies & Anti-CSRF Tokens
How cross-site request forgery rides a logged-in user's cookies to force state-changing requests, and how SameSite, tokens, and double-submit stop it.