Web Application Security

IDOR & Broken Access Control

The #1 OWASP category - horizontal and vertical privilege escalation by tampering with object references.

Medium 24 minidoraccess-controlauthorization

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 = 8847

If 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/123 which has no role check
  • A user POSTing to /api/admin/promote to 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):

ActionExpectedVulnerable Result
Read B's resource as A403 or 404200 with B's data
Update B's resource as A403 or 404200, resource updated
Delete B's resource as A403200, resource deleted
Access admin endpoint as user403200 with admin data
kali@vr4cs: ~
 

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/users leak 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 code

Hands-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=mine

Try:

GET /api/emails?inbox=all
GET /api/emails?userId=1041
GET /api/emails?admin=true

Seen 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 order

2. 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 denied

4. 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 == 403

If 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.