Insecure Direct Object Reference (IDOR) is a class of access-control bug that shows up everywhere: when a server uses an ID from the request to look up a resource but never checks whether the requesting user is authorized to see it. The classic example is GET /invoice/47 — you got your own invoice 47; the next user's invoice 48 is one URL change away.
You are logged in to Heliotrope Defense Systems as alice (user_id=1001). The six endpoints below are real-looking REST API endpoints. Each has an IDOR vulnerability of a different shape. Edit the parameter, hit Send, and see what comes back. When you successfully exploit one, the "Show fix" button reveals the correct authorization pattern.
Your invoice: https://app.example.com/api/invoice/1001 returns your own data. The URL takes an integer. What happens if you change it?
/api/invoice/1002, /1003, etc.invoice = db.invoices.find(id); if (invoice.user_id != session.user_id) return 403;. Use the session, not a parameter, to identify the user. Where ownership is more complex (shared resources, organizations), apply a role-based check. Never trust the ID alone.The team's docs endpoint: /api/docs/{uuid}. Your doc UUID is 3f8a7b9c-2d4e-4a1f-8b6c-1e9d0a2c3f4b. UUIDs are unguessable, right?
... but the developer also added /api/docs?owner_id=1001 as a "convenience" filter.
owner_id=1001 to a different number, like 1002.?owner_id= filter accepted whatever the client claimed. The fix: ignore client-supplied user identifiers entirely. The server already knows who is making the request — pull owner_id from the session, not the query string.Password reset: when you request a reset, the link contains a token like ?token=YWxpY2VfMjAyNi0wNi0wNQ==. Looks opaque. Pretty random.
Use the reset link below to test. Try other base64-encoded values.
alice_2026-06-05. Construct one for another user. echo -n "bob_2026-06-05" | base64 = Ym9iXzIwMjYtMDYtMDU=.username_date). Encoding is not encryption. The fix: generate cryptographically random tokens (e.g., crypto.randomBytes(32)), store the token hash in the database, validate by lookup, expire after a short window, and tie the token to the session that requested it.Update your profile: PUT /api/profile with a JSON body. The form sends name and email. What does the server accept?
"role":"admin" or "user_id":1: /api/profile {"name":"Alice","role":"admin"}.permitted = ['name', 'email']), or define a separate input DTO that doesn't include privileged fields. Frameworks: Rails strong parameters, Django ModelForm field whitelist, Pydantic models in FastAPI.Your project's tasks: GET /api/projects/{project_id}/tasks/{task_id}. The server checks you have access to the project. Does it check the task?
Your project is proj_42. Your task in it is task_500.
task_777) while keeping your own project ID in the URL: /api/projects/proj_42/tasks/777.db.tasks.find({id: task_id, project_id: project_id}). If the task doesn't belong to the project, the lookup returns nothing and the response is 404 — no cross-project leakage.Document downloads are served from https://heliotrope-uploads.s3.amazonaws.com/users/1001/payslip-2026-06.pdf. The presigned URL expires, but the underlying path is the user's ID + filename.
Two attack avenues: predict another user's path, or check whether the bucket itself is misconfigured.
https://heliotrope-uploads.s3.amazonaws.com/users/1002/payslip-2026-06.pdf.The common root cause
All six bugs are variations of one mistake: using a request-supplied identifier to look up data without verifying the caller is authorized to access that specific record. The shape of the identifier changes — integer, UUID, token, S3 path, parent/child relationship — but the missing check is the same.
The universal fix pattern
Every IDOR fix is some variation of this pseudocode:
def get_resource(resource_id, session):
resource = db.find(resource_id)
if not resource:
return 404
if not user_can_access(session.user_id, resource):
return 403
return resource
The user_can_access function is the entire game. Make it correct, make it called everywhere, and IDOR is solved.
- Centralize the check. Authorization scattered across endpoints will always have one endpoint someone forgot. Build a single policy layer (Cedar, OPA, Casbin) and route every authorization decision through it.
- Default deny. An endpoint with no explicit authorization annotation should refuse all requests. Lint for it.
- Test for it. Automated tests should create two users and verify user A cannot access user B's resources via every endpoint. This is one of the highest-value test patterns in any application.
- Don't rely on opacity. UUIDs make enumeration harder; they don't make authorization optional. Encoding (base64) makes no difference at all.
IDOR is the most common access-control vulnerability in modern web apps because it requires nothing technical to exploit — just curiosity about whether changing a number in a URL returns someone else's data. The defenses are equally unglamorous: check authorization on every request, against the session's user identity, with a default-deny policy.
Bug bounty programs pay handsomely for IDOR findings because they keep being found, often in mature, well-funded applications. The pattern is the bug. The pattern is the fix. The discipline is doing it everywhere.
References
Formatted in APA 7. Alphabetized by first author's last name.
- OWASP Foundation. (2021). OWASP top 10: A01:2021 Broken access control. https://owasp.org/Top10/A01_2021-Broken_Access_Control/
- OWASP Foundation. (n.d.). Authorization cheat sheet. https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html
- OWASP Foundation. (n.d.). Insecure direct object reference prevention cheat sheet. https://cheatsheetseries.owasp.org/cheatsheets/Insecure_Direct_Object_Reference_Prevention_Cheat_Sheet.html