IDOR & Broken Access Control
The #1 OWASP category - horizontal and vertical privilege escalation by tampering with object references.
You log into an application, grab your profile URL, change the ID from 1042 to 1041, and you are now reading someone else's private data. No exploit, no tool, no genius. Just arithmetic. Insecure Direct Object Reference (IDOR) and broken access control are, year after year, the most commonly reported class of vulnerability in bug bounty programs - and the most commonly introduced by developers who forget that client-supplied identifiers cannot be trusted.
Do Not Access Other Users' Data
Even in bug bounty programs, accessing real user data to prove an IDOR vulnerability is explicitly prohibited. Create two test accounts you own, demonstrate the vulnerability between them, and stop there. Accessing actual user records - even to prove a bug - can constitute unauthorized access under the CFAA and similar laws and can get your report disqualified.
The Core Problem
An object reference is any identifier that points to a resource: a numeric ID, a filename, a UUID, a username. It is "direct" when the client sends that reference directly in a request and the server fetches the resource without asking "does this user own this?".
GET /api/invoices/8847 → SELECT * FROM invoices WHERE id = 8847If the server returns invoice 8847 to anyone who asks - regardless of who owns invoice 8847 - that is IDOR. The fix is one check: does the authenticated user have permission to read this object?
Horizontal vs. Vertical Privilege Escalation
These two categories describe what you gain from an access control failure:
Horizontal Privilege Escalation
You access resources belonging to another user at the same privilege level. You and the victim are both regular users; you read or modify their data.
Examples:
- Reading another user's messages by changing a conversation ID
- Downloading another user's uploaded documents
- Modifying another user's profile settings
- Resetting another user's API key
Vertical Privilege Escalation
You perform actions reserved for a higher privilege level - you escalate from regular user to admin, or from read-only to write.
Examples:
- A user calling
DELETE /api/admin/users/123which has no role check - A user POSTing to
/api/admin/promoteto grant themselves admin rights - A read-only API key successfully making write operations
- A standard user accessing the admin settings panel by changing a URL parameter
OWASP Broken Access Control - #1 on the Top 10
OWASP's 2021 Top 10 moved Broken Access Control to position 1. IDOR is a subcategory within this class. The category encompasses IDOR, missing function-level access control, CORS misconfigurations, and privilege escalation flaws of all kinds.
Finding IDOR
Step 1: Map Every Object Reference
With Burp Suite proxy active, use the application as a normal user and map every request that contains a reference to a resource you own. Look for:
- Sequential integers:
orderId=1042,ticketId=55,invoiceId=8001 - GUIDs/UUIDs:
documentId=550e8400-e29b-41d4-a716-446655440000 - Usernames or emails in URLs:
/profile/alice,/reports/alice@corp.com - Filenames and paths:
file=reports/Q3-2025.pdf - Encoded values: base64-decoded IDs, hashed identifiers
Step 2: Swap the Reference
Create two test accounts (Account A and Account B). Log in as Account A and capture a request referencing one of A's objects. Then swap the identifier for one of B's objects and replay the request - still authenticated as Account A.
Test matrix (do with two accounts you own):
| Action | Expected | Vulnerable Result |
|---|---|---|
| Read B's resource as A | 403 or 404 | 200 with B's data |
| Update B's resource as A | 403 or 404 | 200, resource updated |
| Delete B's resource as A | 403 | 200, resource deleted |
| Access admin endpoint as user | 403 | 200 with admin data |
Step 3: UUID Bypass Techniques
UUIDs look unguessable, but security through obscurity is not access control. Test:
- Predictable UUIDs: some systems use UUID v1 (timestamp-based) - these are guessable given a few known values and a time window.
- UUID enumeration via API: does
GET /api/usersleak all UUIDs? Does search functionality return UUIDs in results? - Indirect exposure: UUIDs often appear in emails, logs, referrer headers, shared links. Look for them leaking in other responses.
Even with UUIDs, if the server does not check ownership, guessing is just a matter of where you find the UUID rather than whether the check exists.
Step 4: Parameter Pollution and Mass Assignment
Sometimes the object reference is not in the URL but in the request body. Look for:
{"userId": 1042, "role": "user", "email": "me@example.com"}Try adding fields the client should not control:
{"userId": 1042, "role": "admin", "email": "me@example.com"}This is mass assignment - if the server blindly binds all request fields to a model object, adding role: admin can promote yourself. Common in Rails (strong parameters misconfiguration), Django, Spring Boot, and Node/Express apps.
Step 5: HTTP Method Switching
Some authorization checks only cover specific HTTP methods. Try:
GET /api/users/1041 → 403 (correct)
POST /api/users/1041 → 200 (missing check on POST)
PUT /api/users/1041 → 200
DELETE /api/users/1041 → 200
HEAD /api/users/1041 → leaks headers, response codeHands-on Lab
IDOR Account Takeover
In this lab you will exploit an IDOR vulnerability in a realistic social platform to perform a full account takeover. Starting from two test accounts, you will identify the numeric user ID exposed in the settings API, replay an authenticated password-change request with the victim's ID substituted, and log in as the victim with the new password. A second stage extends to a vertical escalation: the admin panel's user-management API is missing a role check, allowing a regular user to grant admin rights to their own account.
Indirect IDOR and Chained Bugs
IDOR becomes more powerful when chained with other bugs:
IDOR + PII leak: A user ID in an invoice API exposes name, address, and payment method.
IDOR + account takeover: An API lets you change the email address of any account by ID, with no confirmation email. Change it, trigger "forgot password", and log in.
IDOR + stored XSS: You can write to another user's profile fields. Plant a stored XSS payload.
IDOR + SSRF: An admin import feature accepts a URL. You can trigger it on behalf of any user by replacing the user ID in the request.
Real-World Patterns
Numeric ID Ranges
Many applications start IDs at 1 and increment. If you see userId=1042, IDs 1 through 1041 exist. Automated scanning with Burp Intruder or a script across this range can confirm mass IDOR. Do this only with test accounts you control.
Object IDs in JWT Claims
Applications sometimes embed the object reference in a JWT payload:
{"sub": "user_1042", "role": "user", "orderId": "8847"}If the server trusts the orderId claim without re-verifying ownership, manipulating the JWT (if the signature is weak or the alg=none bypass works - see the Authentication lesson) gives you arbitrary object access.
Hidden Parameters
Some APIs accept optional filter parameters the UI never uses:
GET /api/emails?inbox=mineTry:
GET /api/emails?inbox=all
GET /api/emails?userId=1041
GET /api/emails?admin=trueSeen in the wild · PayPal
Real HackerOne breakdown
A researcher discovered that PayPal's API for managing secondary account users (sub-accounts added to a business account) allowed any authenticated PayPal user to enumerate and access another business account's sub-user list by supplying a different account ID in the API request. The endpoint had no ownership check - it returned the sub-user list for whatever account ID was supplied. PayPal patched the missing authorization check and paid a bounty. The fix was adding a server-side check confirming the requesting user owned the parent account before returning sub-user data.
Prevention
1. Server-Side Authorization on Every Request
The golden rule: never trust the client's claimed identity for authorization. Extract the user's identity from the authenticated session - not from the request body or URL - and check ownership in the data layer.
# VULNERABLE - trusts client-supplied userId
@app.get("/api/orders/{order_id}")
def get_order(order_id: int, user_id: int = Query(...)):
return db.get_order(order_id) # no check
# SECURE - extracts user from session, verifies ownership
@app.get("/api/orders/{order_id}")
def get_order(order_id: int, current_user: User = Depends(get_current_user)):
order = db.get_order(order_id)
if order.user_id != current_user.id:
raise HTTPException(status_code=403, detail="Forbidden")
return order2. Use Indirect References Where Possible
Instead of exposing database primary keys, create a per-session mapping:
# Generate a session-scoped token for each resource
session["invoice_tokens"]["inv_abc123"] = 8847
# Client submits "inv_abc123"; server looks up the real ID
real_id = session["invoice_tokens"].get(submitted_token)This prevents enumeration even if authorization checks are imperfect, but it is defense-in-depth, not a replacement for proper checks.
3. Centralized Authorization
Avoid scattering if user.role == 'admin' checks across every route. Use a centralized authorization layer (attribute-based access control, policy objects, or a library like Casbin or OPA) so checks cannot be accidentally omitted.
# Using a policy layer
authorize(current_user, "read", order) # raises 403 if denied4. Test During Development
Include IDOR tests in your integration test suite:
def test_cannot_read_other_users_order(client, user_a, user_b):
order = create_order(user_b)
response = client.get(f"/api/orders/{order.id}",
headers=auth_headers(user_a))
assert response.status_code == 403If this test does not exist, the check might not either.
5. Avoid Predictable Identifiers
Use UUIDs v4 (random) instead of sequential integers for user-facing IDs. This raises the bar for enumeration - though it is not a substitute for authorization checks.
Key Takeaways
- IDOR occurs when a server fetches a resource by client-supplied ID without verifying the requesting user owns it.
- Horizontal escalation: access another user's data at the same privilege level. Vertical escalation: perform actions reserved for higher-privileged roles.
- Hunt by mapping all object references in requests, swapping them between two test accounts you own, and testing every HTTP method and request parameter.
- UUIDs are not security - they raise the cost of enumeration but do not replace authorization checks.
- Fix IDOR by extracting user identity from the authenticated session, checking ownership server-side on every request, and writing authorization tests that will catch regressions.