IDOR Testing: A Complete Guide to Finding Insecure Direct Object References
IDOR is consistently one of the most commonly reported findings in bug bounty programs and one of the most underreported findings in automated scans. The vulnerability is conceptually simple — you replace one user's ID with another's and see if the server returns something it shouldn't — but finding it systematically requires understanding how an application maps users to data.
Unlike injection vulnerabilities, where the presence of special characters in a response confirms the bug, IDOR requires a semantic judgment: is this response something the requesting user is actually authorized to see? That's why automated scanners struggle with it, and why it remains a reliable source of high-severity findings for manual testers and bug hunters.
What is an IDOR?
IDOR (Insecure Direct Object Reference) is a type of broken access control (OWASP A01) where an application uses a user-controlled identifier — like a numeric ID, filename, or hash — to access objects without checking whether the requesting user has permission to access that specific object.
The canonical example: GET /api/invoices/1042. Change 1042 to 1041 and if the response contains a different user's invoice data, that's an IDOR. The server found the record and returned it — it just never asked whether the requestor owned it.
IDOR is "direct" because the reference is unmediated. There's no indirection layer between the client-supplied value and the database lookup. The server trusts the client to only reference objects they're allowed to see, and that trust is the vulnerability.
This sits under OWASP A01:2021 (Broken Access Control), the top-ranked category for good reason. Access control failures are pervasive, difficult to detect automatically, and high-impact when exploited — you're not crashing a service, you're reading, modifying, or deleting another user's data.
Horizontal vs Vertical IDOR
Access control failures in IDOR fall into two distinct patterns, and it's worth being precise about which one you're dealing with because the remediation differs.
Horizontal IDOR is far more common. Applications typically implement role-based access control (RBAC) to gate admin-vs-user functionality, but they often skip the per-object ownership check that would catch horizontal access. The developer assumes that users will only ever send their own IDs — an assumption that holds exactly until someone opens Burp Suite.
Vertical IDOR tends to be higher severity: a horizontal IDOR exposes one user's data, while a vertical IDOR may expose administrative functions or the entire dataset. Both are exploitable from an unauthenticated or low-privilege position with nothing more than a modified request.
Types of Object References to Test
Before you can test for IDOR, you need to enumerate every parameter in the application that references a resource. The reference type determines your testing approach.
Sequential integer IDs — userId=1042, invoiceId=8734, /orders/19283. The simplest case. Increment or decrement the value and observe whether the server returns a different user's object. Sequential integers are trivially enumerable and the most reliably detected by automated tools.
Predictable composite IDs — orderNumber=2024-06-001, filename=report_john.pdf, exportId=export_20240601_acme. Not random, but not sequential integers. Requires understanding the naming pattern, which often leaks in other responses.
Encoded IDs — Base64-encoded integers are not security. dXNlcklkPTEwNDI= decodes to userId=1042. Encoding is not encryption.
import base64
# Decode the value you intercepted
encoded = "dXNlcklkPTEwNDI="
print(base64.b64decode(encoded).decode())
# → userId=1042
# Modify and re-encode
modified = base64.b64encode(b"userId=1041").decode()
print(modified)
# → dXNlcklkPTEwNDE=
Send the re-encoded value in the original parameter and observe whether the response changes. This pattern appears far more often than it should in production APIs.
GUIDs/UUIDs — Not inherently secure. UUIDs are designed for uniqueness, not secrecy. They can be leaked through other API responses (creator metadata, activity logs, audit endpoints), URL bars in shared screenshots, error messages, or weak generation (UUIDv1 embeds a timestamp and MAC address — enumerable). When testing GUID-based endpoints, collect UUIDs from every other part of the API first.
Hashed IDs (MD5/SHA1 of sequential integers) — Trivially reversible for small integer spaces. md5(1042) is a fixed, known value. An attacker who discovers the scheme can precompute a full lookup table for the integer range in seconds.
import hashlib
# If the application uses md5(user_id) as its reference
for uid in range(1000, 1100):
h = hashlib.md5(str(uid).encode()).hexdigest()
print(f"{uid} → {h}")
Indirect object references via filenames — /api/download?filename=q4-report-acme.pdf, /exports/invoice_march_2024.csv. Semantic filenames require understanding the application's naming convention, but are often discoverable from other responses or by inferring patterns from your own generated files.
Where to Look for IDOR Vulnerabilities
Systematically enumerate every parameter that references a resource. IDOR is not confined to URL path parameters — it appears anywhere a client-supplied value is used to look up a server-side object.
- URL path parameters:
/users/{id}/profile,/orders/{id}/cancel,/documents/{id}/download. The most visible location and the first place to check. - Query strings:
?fileId=8234,?reportId=acme-q4,?customerId=1042. Common in GET requests for fetching or filtering resources. - Request body (JSON/form data):
{ "targetUserId": 1042 },invoiceId=8734in a POST body. Often overlooked — scanners that only fuzz URL parameters miss these entirely. - HTTP headers:
X-User-Id,X-Account-Id,X-Tenant-Id. Some APIs use custom headers to route requests to the correct tenant or user context. These are almost never validated against the session. - Cookies: Session tokens or preference cookies that contain base64-encoded user IDs or account identifiers embedded in the cookie value itself.
- File operations: Upload, download, delete, and rename endpoints. File download endpoints are particularly high-value — a successful IDOR here means arbitrary file access across the entire user base.
When you're mapping the application, capture every API response too. Objects returned from one endpoint often embed IDs from other objects — another user's message thread ID, a document ID, a customer record reference — that you can then test against endpoints you've catalogued.
Testing Methodology
Systematic IDOR testing requires two accounts and a methodical approach to comparing access. Here's the full sequence.
Step 1: Map the application. Catalog all API endpoints and identify those that accept resource identifiers. Use your browser's developer tools network panel, a Burp Suite proxy, or automated endpoint discovery. For each endpoint, note the HTTP method, the identifier parameters, and what resource type they reference.
Step 2: Authenticate as two accounts. Create Account A and Account B — preferably in the same role tier to test horizontal IDOR first. As each account, create resources: invoices, messages, reports, documents, exports. Record the object IDs that each account's resources are assigned.
Step 3: Cross-account access test. Using Account A's session token, request Account B's object IDs. Using Account B's session token, request Account A's objects. A 200 response returning the resource content confirms the IDOR. A 403 or 404 means access control is working for this endpoint.
Step 4: Parameter mining. Don't limit yourself to IDs visible in the URL. Use your browser developer tools or Burp to inspect full API response bodies — they frequently contain object IDs that never appear in the interface. An invoice list response might embed customerId, billingAddressId, and taxProfileId fields, all of which are separately testable.
Step 5: Fuzz incrementally for numeric IDs. If your account's object ID is 1042, test 1041, 1040, 1043, 1044. For a new application, also test low integers (1, 2, 3) — early records are often seed data or admin-created objects that regular users shouldn't access.
Step 6: Test CRUD operations separately. An application might correctly block GET /api/invoices/1041 (read) but allow DELETE /api/invoices/1041 (delete) or PUT /api/invoices/1041 (modify) for objects you don't own. Access control is often implemented at the controller level for reads and forgotten for mutations.
# Account A creates a resource
curl -s -X POST /api/documents \
-H "Authorization: Bearer TOKEN_A" \
-H "Content-Type: application/json" \
-d '{"title":"My confidential doc","content":"..."}' \
| jq '.id'
# → "doc_8734"
# Account B tries to access it
curl -s -X GET /api/documents/doc_8734 \
-H "Authorization: Bearer TOKEN_B"
# If this returns the document, it's an IDOR
# Also test mutating operations with Account B's token
curl -s -X DELETE /api/documents/doc_8734 \
-H "Authorization: Bearer TOKEN_B"
# A 200 here means write-path IDOR even if read was blocked
curl -s -X PUT /api/documents/doc_8734 \
-H "Authorization: Bearer TOKEN_B" \
-H "Content-Type: application/json" \
-d '{"title":"Tampered by Account B"}'
# 200 here means you can modify other users' objects
What Automated Tools Find and Miss
Being precise about scanner capabilities here matters — IDOR is the vulnerability class where the gap between automated and manual testing is widest.
What automated scanners detect reliably:
- Sequential integer ID tampering. The test is deterministic: increment the integer, compare the response to the authenticated user's expected data. If the structure matches but the content differs (different email address, different name), flag it.
- Encoded ID patterns. Base64 decode, increment the underlying integer, re-encode, send. Scanners that understand common encoding schemes handle this automatically.
- Error-based leakage from invalid ID formats. Sending
../../../etc/passwdor0or-1in an ID field and observing verbose error messages that confirm object existence or reveal internal identifiers.
What automated scanners miss — and why you need manual testing:
- GUIDs/UUIDs. Scanners can't enumerate what they don't know. They don't have Account B's UUIDs. Testing GUID-based IDOR requires two authenticated sessions where you've captured the object identifiers from each.
- Indirect references and semantic filenames. A scanner doesn't know that
q4-report-acme.pdfbelongs to the Acme account and that you should tryq4-report-globex.pdf. Understanding the naming convention requires reading the application. - Time-of-check vs time-of-use (TOCTOU) IDORs. Access control is checked at request time, but the object is accessed slightly later in a concurrent flow — a race condition in the authorization logic that requires coordinated parallel requests.
- Multi-step flow IDORs. Step 1 initiates an export job for your own data. Step 3 downloads the export by job ID. If the job ID is predictable, you can download another user's export — but only after they've initiated their own export. A scanner testing step 3 in isolation will miss this.
- CRUD-specific access control gaps. Blocked on GET but allowed on DELETE. Scanners that test read operations may not exercise every HTTP method against every endpoint.
Ironimo's scanner handles sequential ID and encoded ID patterns automatically, testing them against every discovered endpoint on each scan. For GUID-based and indirect-reference IDOR, the scanner flags the relevant endpoints as requiring manual cross-account review and provides step-by-step test guidance — so you know exactly where to focus your manual effort.
| IDOR Type | Automated Detection | Notes |
|---|---|---|
| Sequential integer IDs | Yes | Deterministic — increment and compare response structure |
| Base64-encoded integer IDs | Yes | Decode, increment, re-encode, send |
| MD5/SHA1 of sequential integers | Partial | Detectable if the hashing scheme is identified |
| GUIDs/UUIDs | No | Requires two authenticated sessions with known object IDs |
| Semantic filenames | No | Requires understanding the application's naming convention |
| Multi-step flow IDOR | No | Requires state from an earlier step; scanners test endpoints in isolation |
| CRUD-specific (DELETE/PUT blocked on GET) | Partial | Depends on whether scanner tests all HTTP methods per endpoint |
| Custom header IDs (X-User-Id) | Partial | Requires header-aware scanning; often missed by URL-focused tools |
Real-World IDOR Examples
IDOR shows up in production across every industry and application type. These patterns are drawn from publicly disclosed bug bounty reports and post-incident analyses.
Ride-sharing trip receipts (2019). A ride-sharing platform's receipt download endpoint accepted a numeric trip ID in the URL: /receipts/download/{tripId}. Trip IDs were sequential integers. Authenticated users could increment the trip ID to retrieve full receipts for any trip taken on the platform — including passenger name, pickup address, drop-off address, and payment amount. The fix was adding an ownership check: verify that the authenticated user's account ID matched the passenger or driver on the requested trip before serving the receipt.
Health platform patient documents. A patient portal used UUIDs for document references: GET /documents/{uuid}. The UUIDs were not sequential and appeared random. The vulnerability wasn't in the ID generation — it was in how the application surfaced IDs. The doctor-facing portal displayed document preview links in the browser's URL bar. Doctors frequently shared screenshots of their portal for support tickets. Those screenshots exposed patient document UUIDs. Additionally, the UUID appeared in server-side rendered page source where an XSS on the doctor portal could harvest it. Once a UUID was known, any authenticated user — patient or physician — could retrieve the document without any ownership check.
E-commerce order export. An e-commerce platform offered a bulk order export feature for sellers: POST /api/exports/orders with a JSON body containing { "orderId": 19283 }. The endpoint was authenticated — you needed a valid seller session — but it did not verify that the order belonged to the authenticated seller's account. Any seller could export the full order details of any order on the platform by iterating through sequential order IDs. Full order history — customer names, addresses, items purchased, payment method type — was accessible to any authenticated seller. The endpoint had role-based access control (sellers only, not buyers) but lacked object-level access control.
Prevention
The fixes for IDOR are well-understood. The gap is implementation discipline — access control has to be applied consistently at the data layer, not just at the route level.
Never rely on the identifier alone. The presence of a valid ID in a request is not authorization. Every query that retrieves an object by ID must also verify that the authenticated user has permission to access that specific object. This check belongs in your data access layer, not in a controller that might be bypassed by a different route.
Use indirect object references. Rather than exposing real database IDs to the client, maintain a session-scoped mapping: myInvoices[3] maps to invoice ID 8734 stored server-side in the session. The client can only reference objects that the server has explicitly included in their session map. This prevents enumeration entirely — an attacker can't guess an ID that was never sent to them.
Prefer UUIDs over sequential integers — but understand what that buys you. UUIDs reduce the risk of enumeration attacks, but they are not access control. A GUID is harder to guess, not impossible to discover. Switching to UUIDs while skipping the authorization check just trades a trivially exploitable IDOR for a slightly harder one. Use UUIDs to reduce attack surface; add authorization checks for actual security.
Authorization middleware at the data layer. Apply access control in your ORM or repository layer so that every query is automatically scoped to the authenticated user. A findById call that silently adds a WHERE owner_id = :currentUser clause to every query is harder to forget than a manual check in a controller. This pattern prevents entire classes of access control bypass.
Log access attempts for anomaly detection. When a user's session requests an object ID that wasn't issued to them in any prior response, flag it. This won't prevent the vulnerability, but it surfaces exploitation attempts and lets you detect breaches after the fact — critical for regulatory compliance and incident response.
def get_invoice(invoice_id: str, current_user: User) -> Invoice:
invoice = db.invoices.find_by_id(invoice_id)
if invoice is None:
raise NotFound()
if invoice.owner_id != current_user.id:
# Log the access attempt — this is useful signal
audit_log.warning(
"IDOR attempt",
user_id=current_user.id,
requested_invoice_id=invoice_id,
actual_owner_id=invoice.owner_id
)
raise Forbidden() # Not NotFound — be explicit in logs
return invoice
Note the distinction between returning Forbidden versus NotFound on an ownership failure. From a security-through-obscurity perspective, NotFound leaks less information to an attacker — they can't tell whether the object exists. From an operational perspective, Forbidden gives you cleaner logs. In most threat models, logging Forbidden with the full context is the better choice: the attacker already knows the object exists (they found the ID somewhere), and your detection capability is more valuable than marginal obscurity.
Ironimo scans your running application for access control vulnerabilities including IDOR patterns — automatically, on every deployment. Sequential ID enumeration, encoded ID testing, and endpoint-level CRUD coverage come standard. For GUID-based and multi-step IDOR, Ironimo flags the endpoints and provides manual test guidance so your team knows exactly where to look.
Start free scan