In 2019, a security researcher discovered that a major U.S. banking API allowed any authenticated user to retrieve the transaction history of any other user's account by changing the account_id parameter in the request. The API validated that the caller was authenticated (they had a valid session token), but it did not validate that the caller was authorized to access the specific account they were requesting. The fix was a single line of code , adding a check that the account belonged to the authenticated user , but the vulnerability had been in production for years, exposing the financial records of millions of customers to any authenticated user who knew or guessed another customer's account identifier.
This is the essence of IDOR: the system confirms who you are but fails to confirm whether you should have access to what you're asking for. It is the most common form of broken access control (OWASP's #1 category in its Top 10 since 2021), and it persists because it is not a coding error in the traditional sense. It is a missing check , the absence of code rather than the presence of bad code , which makes it invisible to the tools and processes that catch most other vulnerability classes.
The Structural Cause
IDOR vulnerabilities exist because of a fundamental asymmetry in how APIs are typically built. Authentication , confirming the caller's identity , is a cross-cutting concern implemented once, in middleware or a framework, and applied to all endpoints. Authorization , confirming the caller's right to access a specific resource , is a per-endpoint, per-resource concern that must be implemented individually for every access path.
This asymmetry means that a developer can build an entire API with correct authentication, request validation, input sanitization, and error handling, and still have IDOR vulnerabilities on every endpoint because authorization was not explicitly implemented. The API "works correctly" in the sense that it serves the right data to the right request , it just serves it to the wrong person.
The problem is worse in RESTful APIs because the URL structure exposes the resource identifiers:
GET /api/accounts/12345/transactions
GET /api/users/67890/profile
GET /api/invoices/11111/download
Each path segment containing a numeric ID is a potential IDOR target. The attacker does not need to discover these identifiers through reconnaissance , they are in the URL, often sequential, and easily enumerable.
Why Traditional Security Tools Miss IDOR
Static analysis cannot detect IDOR because it cannot determine whether a given code path should or should not require authorization. The scanner sees a database query filtered by account_id from the request parameter. Is this correct? It depends on whether the business logic requires that the caller owns the account , a semantic property that no static analysis tool can infer.
Dynamic application security testing (DAST) can detect IDOR only if it is specifically configured with multiple user accounts and instructed to test cross-account access. A single-user DAST scan will not find IDOR because every request from the scan user is authorized for the scan user's resources. The scanner needs to authenticate as User A and attempt to access User B's resources , a test that requires understanding of the application's data model and user relationships.
Web Application Firewalls (WAFs) cannot detect IDOR because IDOR requests are syntactically identical to legitimate requests. The malicious request GET /api/accounts/12345 looks exactly the same as the legitimate request GET /api/accounts/12345 , the difference is who is making the request, which the WAF generally does not evaluate.
This is why IDOR consistently ranks as the most common finding in penetration tests and bug bounty programs. It requires manual or semi-manual testing with multiple user accounts, and it must be tested individually for every endpoint that accepts a resource identifier.
The Exploitation Is Trivially Easy
IDOR exploitation requires no special tools, no exploit code, and no technical sophistication. An authenticated user changes a number in a URL or request body and observes whether the server returns data belonging to another user. The entire "exploit" is a modified HTTP request:
The severity depends on the sensitivity of the exposed data and the scope of the enumeration. If account IDs are sequential integers, the attacker can trivially iterate through every account: a simple loop from 1 to N extracts the data of every user in the system. If identifiers are UUIDs, enumeration is harder but not impossible , UUIDs may leak through other API responses, error messages, email links, or client-side JavaScript.
Authorization Architecture That Eliminates IDOR at the Layer Level
The most reliable defense against IDOR is not adding authorization checks to individual endpoints (though that is necessary in the short term). It is building the authorization check into the data access layer so that it is impossible to retrieve a resource without ownership validation.
Implicit scoping through authenticated context. Instead of accepting a resource identifier from the request and then checking ownership separately, derive the resource scope from the authenticated user's identity:
# Vulnerable: accepts account_id from request, checks nothing
@app.get("/api/accounts/{account_id}/transactions")
def get_transactions(account_id: int, token: AuthToken):
return db.query("SELECT * FROM transactions WHERE account_id = %s", [account_id])
# Secure: derives scope from authenticated user
@app.get("/api/my/transactions")
def get_my_transactions(token: AuthToken):
account_id = get_account_for_user(token.user_id)
return db.query("SELECT * FROM transactions WHERE account_id = %s", [account_id])In the secure version, the account_id is never supplied by the caller. It is derived from the authenticated user's token. The IDOR is structurally impossible because there is no user-controllable identifier to manipulate.
This pattern is not always applicable , some APIs legitimately need to accept resource identifiers (admin APIs, APIs where one user has delegated access to another user's resources). But for the common case where "a user accesses their own data," implicit scoping eliminates the vulnerability class entirely.
Policy engine enforcement. For APIs that must accept resource identifiers, a centralized policy engine (OPA, Casbin, or a custom authorization service) evaluates every access against explicit rules:
allow {
input.method == "GET"
input.path == ["api", "accounts", account_id, "transactions"]
data.account_owners[account_id] == input.user_id
}
The policy engine sits between the API handler and the data access layer. No data is returned unless the policy evaluates to allow. This centralizes the authorization logic (so it is reviewed and tested once, not implemented independently on every endpoint) and ensures that new endpoints are covered by default.
Non-enumerable identifiers. Replacing sequential integer IDs with UUIDs or opaque tokens does not fix IDOR , it makes enumeration harder. An attacker who cannot guess the next account ID cannot iterate through all accounts. But UUIDs can leak through other channels, so this is a defense-in-depth measure, not a fix. The authorization check must still exist.
The Organizational Challenge
IDOR is difficult to eliminate because it requires explicit, correct authorization logic on every endpoint that accesses a resource, and because that logic must be kept in sync with the evolving data model and access patterns. A new API endpoint added by a developer who is focused on functionality and unaware of the authorization pattern is a new potential IDOR. A refactored data model that changes the ownership relationship may invalidate existing authorization checks.
The organizations that have the fewest IDOR vulnerabilities are not the ones with the best developers. They are the ones with the best architectural enforcement: authorization logic that is centralized, mandatory, and default-deny, so that a new endpoint without explicit authorization rules cannot access any data at all. The default is not "access allowed until proven otherwise." The default is "access denied until an explicit policy grants it."
IDOR is the vulnerability class that proves a simple truth about API security: authentication without authorization is access control theater. Knowing who the caller is means nothing if you do not check what they are allowed to access. And checking what they are allowed to access, for every resource, on every endpoint, consistently and correctly , that is the hardest problem in application security, not because the code is complex, but because the discipline must be universal.

