Description
An insecure direct object reference occurs when an application uses a user-controllable value — an ID, filename, or other reference — to retrieve or operate on a resource without first verifying that the requesting user has permission to do so.
The fix is often a single missing ownership check. The impact is complete horizontal access to every other user's data.
IDOR is consistently the most commonly found vulnerability class in modern web application pentests. It is also consistently underestimated: because each individual instance affects only the data behind one predictable ID, developers sometimes treat it as low-risk. In practice, sequential integer IDs mean an attacker can enumerate every record in the system.
How It Works
A typical example: a user retrieves their own invoice at /api/v2/invoices/1842. The API returns the invoice because the user is authenticated — but the server never checks whether invoice 1842 belongs to this user's account. The attacker increments the ID:
GET /api/v2/invoices/1843
Authorization: Bearer <attacker_token>
HTTP/1.1 200 OK
{"id": 1843, "tenant": "AcmeCorp", "amount": 48000, ...}
The server returns another tenant's invoice with no error.
IDOR is not limited to GET requests. The same class of bug appears in:
- PUT/PATCH — updating another user's record.
- DELETE — deleting another user's data.
- File download — accessing attachments by guessing predictable filenames.
- Password reset — using another user's reset token if they are sequentially generated.
Impact
- Horizontal privilege escalation — access or modify any other user's data at the same permission level.
- Vertical privilege escalation — in some cases, access or modify admin-owned records.
- Mass data exfiltration — enumerate sequential IDs to scrape an entire dataset.
- Tenant isolation bypass — in multi-tenant SaaS applications, read or modify data belonging to other organisations.
- Compliance violations — GDPR, HIPAA, and PCI DSS all require access controls that prevent this.
Detection
IDOR testing requires an authenticated testing approach with at least two independent accounts at the same privilege level.
- Enumerate all object references in the application — IDs in URL paths, query parameters, POST body fields, and response bodies that reference other objects.
- Swap identifiers between accounts — obtain a resource ID from Account A, then attempt to access it from Account B with no account-A credentials.
- Test all HTTP methods — an endpoint that correctly restricts
GETmay allowPUT,DELETE, orPATCHwithout the same check. - Test indirect references — references embedded in JWTs, signed tokens, or response objects may also be substitutable if the signature is not validated or reused.
Remediation
Server-side ownership validation on every request that accesses a user-scoped resource. The check must happen in the data layer, not just the route handler.
# Vulnerable
invoice = Invoice.get(request.params['id'])
return invoice
# Safe
invoice = Invoice.get(request.params['id'])
if invoice.tenant_id != current_user.tenant_id:
raise Forbidden()
return invoice
Indirect object references. Map the real internal ID to a user-scoped token that has no meaning outside the context of the authenticated session. The user never sees the database ID.
Centralise access control logic. Ad-hoc ownership checks scattered across the codebase will have gaps. An authorisation layer (policy objects, middleware) that every data access passes through is far more auditable.
