IDOR (Insecure Direct Object Reference) occurs when an application uses user-controlled input to access objects directly without authorization checks.
TL;DR
An Insecure Direct Object Reference (IDOR) occurs when an application uses user-controlled input — a URL path segment, query parameter, request body field, or HTTP header — to look up an internal object directly, without verifying that the requesting user is authorized to access that specific object. The application authenticates the user correctly but skips the second step: confirming that the authenticated identity owns or has been granted access to the requested resource.
IDOR is classified as CWE-639 (Authorization Bypass Through User-Controlled Key) and is the primary vulnerability class within OWASP A01:2021 and A01:2025 — Broken Access Control. In the API security context, its equivalent is called BOLA (Broken Object Level Authorization), which holds position API1:2023 in the OWASP API Security Top 10. The distinction is terminological: IDOR and BOLA describe the same root cause applied in web and API contexts respectively.
The vulnerability persists at scale because most authentication frameworks enforce identity correctly, but object-level authorization — verifying that this identity may access this specific record — requires deliberate, per-endpoint implementation. When developers assume that authentication is sufficient, or when authorization logic is applied in the UI but not the API layer, IDOR results.
The authorization gap has three root causes in production code:
WHERE owner_id = ? clause.role, is_admin, tenant_id) the user should never control.# VULNERABLE — authenticates but does not authorize
@router.get("/api/invoices/{invoice_id}")
async def get_invoice(invoice_id: int, user: User = Depends(get_current_user)):
invoice = await db.get(Invoice, invoice_id) # fetches ANY invoice
return invoice
# SAFE — ownership enforced at the database query level
@router.get("/api/invoices/{invoice_id}")
async def get_invoice(invoice_id: int, user: User = Depends(get_current_user)):
invoice = await db.execute(
select(Invoice)
.where(Invoice.id == invoice_id)
.where(Invoice.owner_id == user.id) # ownership gate
)
if not invoice:
raise HTTPException(status_code=404) # 404 not 403 — prevents enumeration oracle
return invoice| Variant | Core Mechanism | Typical Impact | CVSS Range |
|---|---|---|---|
| Direct Object Reference | Sequential integer ID in URL/body — increment to access peer records | PII exposure, financial data | 6.5–8.8 |
| Horizontal Privilege Escalation | Same-role user accesses peer's resources by substituting their identifier | Cross-user data access, BFLA write | 6.5–8.8 |
| Vertical Privilege Escalation | Regular user calls admin endpoints or obtains admin object references | System-wide compromise, account takeover | 7.5–9.1 |
| Indirect Object Reference | Opaque token (hash, UUID v1, slug) maps to a record but is guessable or reversible | Varies by reference type | 5.0–7.5 |
| IDOR via Mass Assignment | Request body fields bind to protected model properties — role, is_admin, balance | Privilege escalation, financial fraud | 7.5–9.0 |
The simplest form: auto-incremented database primary keys appear in URLs or request bodies. An attacker increments or decrements the ID to access adjacent records. The Optus breach (2022) exploited exactly this pattern — GET /api/customers/1 through GET /api/customers/9800000 extracted 9.8 million records without authentication.
User A and User B share the same role. User A substitutes B's resource identifier into a request authenticated as A. The application validates A's session but does not check whether A owns the referenced resource. This is the most common IDOR subtype and accounts for the majority of high-severity bug bounty reports. The McDonald's McHire breach (July 2025, 64 million records) was a horizontal IDOR on the lead_id parameter.
A regular user invokes endpoints or functions reserved for privileged roles. Unlike horizontal IDOR — which is about accessing peer data — vertical escalation is about invoking privileged functions. CVE-2025-27507 in the ZITADEL identity platform (CVSS 9.0) allowed non-admin users to call 12 Admin API gRPC endpoints, including LDAP configuration modification, achieving full account takeover for all LDAP users on the instance.
The application uses a secondary identifier — a filename, reference code, hash, or opaque token — that appears non-guessable but is reversible or enumerable. md5(user_id) is trivially computable. Base64-encoded IDs decode immediately. UUID v1 timestamps are predictable within a clock window. The educational point: opacity is not authorization.
API frameworks that bind request body fields directly to ORM model objects allow attackers to set fields the application never intended to be user-writable: role, is_admin, account_balance, tenant_id. CVE-2024-7297 in Langflow (CVSS 8.8) allowed any authenticated user to gain super-admin access by sending {"is_superuser": true} to a PATCH endpoint.
Optus (September 2022) — A REST API endpoint /api/customers/{id} returned customer records keyed by sequential integers with no authentication. GET /api/customers/1 through 9.8 million iterations exfiltrated names, dates of birth, passport numbers, and driver's license numbers. Regulatory outcome: AUD $1.36 million ACMA fine.
McDonald's McHire (July 2025) — Researchers Ian Carroll and Sam Curry found that the lead_id parameter on /api/lead/cem-xhr was a sequential integer. Decrementing or incrementing it returned another applicant's full chat transcript including name, phone, email, and personality test outcomes. Scale: 64 million records. Remediated within 24 hours of disclosure.
CVE-2025-27507 — ZITADEL (CVSS 9.0, Critical) — 12 HTTP endpoints in the Admin API enforced org-level IAM permissions instead of system-level permissions. Authenticated non-admin users modified LDAP configuration, redirecting authentication to a malicious server and intercepting credentials for all LDAP users on the instance. Affected ZITADEL 2.x across eight version branches.
HackerOne #2122671 — HackerOne Platform ($12,500) — A GraphQL mutation allowed deletion of any user's certifications and licenses. The object id field in the mutation was not validated against the caller's identity. The platform that runs bug bounty programs was itself vulnerable to IDOR.
HackerOne #415081 — PayPal ($10,500) — Parameter manipulation on /businessmanage/users/api/v1/users granted secondary account creation on victim business accounts, enabling account takeover.
/api/orders/1338), query string (?order_id=1338), request body ({"order_id": 1338}), and HTTP headers (X-User-ID: 1338).role, is_admin, balance). Follow up with a GET to verify persistence.The Burp Suite Autorize extension automatically replays every intercepted request with a second user's session cookie, color-coding responses by access control status. AuthMatrix defines a role × endpoint permission matrix and tests all combinations. Automated tools have a fundamental limitation: they cannot reason about object ownership without human-defined resource relationships.
BreachVex detects IDOR through a multi-signal differential approach: it probes adjacent IDs, compares response bodies across two authenticated sessions, and corroborates findings across complementary signals — a status-code differential (403→200), ID echo in the response body, PII field divergence, and JSON structure comparison. Mass assignment detection injects a broad privilege-field wordlist into POST/PUT/PATCH endpoints and confirms via a GET re-read.
Returning HTTP 403 on unauthorized access confirms the resource exists, enabling enumeration oracles. Always return 404 for unauthorized object access — this forces attackers to distinguish "does not exist" from "exists but forbidden" without a deterministic signal.
Derive user identity exclusively from the authenticated session. Every query on a user-owned resource must include the ownership constraint structurally — not as an optional check that a future developer might omit.
-- VULNERABLE — fetches any record by ID
SELECT * FROM orders WHERE order_id = ?;
-- SAFE — ownership is a structural query constraint
SELECT * FROM orders WHERE order_id = ? AND owner_id = ?;
-- Multi-tenant: add tenant scoping
SELECT * FROM orders WHERE order_id = ? AND owner_id = ? AND tenant_id = ?;Do not expose auto-increment integers in public APIs. UUID v4 (cryptographically random, 2¹²² possible values) prevents enumeration-based IDOR. UUID v1 (timestamp-derived) is partially predictable and must be avoided for security tokens (CVE-2024-45719). UUIDs are not a substitute for authorization — they are rate-limiting controls on guessing.
Use schema validation that declares exactly which fields are writable. Fields not in the schema cannot be submitted regardless of what the client sends.
# FastAPI / Pydantic — only declared fields are writable
class UserUpdateRequest(BaseModel):
display_name: str
bio: str | None = None
# is_admin, role, tenant_id: NOT declared → rejected automatically
@router.patch("/users/me")
async def update_me(user_id: str = Depends(get_current_user_id),
data: UserUpdateRequest):
await db.users.update(id=user_id, **data.model_dump(exclude_unset=True))RLS enforces ownership at the database layer, preventing IDOR even if application code has a defect:
ALTER TABLE orders ENABLE ROW LEVEL SECURITY;
CREATE POLICY orders_isolation ON orders
USING (owner_id = current_setting('app.current_user_id')::int);
-- In FastAPI connection setup: SET app.current_user_id = {user.id}Google's Zanzibar paper (2019) introduced Relationship-Based Access Control (ReBAC) — modeling access as graph relationships between users and resources. Open-source implementations include OpenFGA (CNCF Incubating), SpiceDB, and Warrant. ReBAC naturally prevents cross-tenant IDOR because access is defined by explicit relationship edges, not parameter matching.
An IDOR vulnerability occurs when an application uses user-controlled input — a URL parameter, request body field, or header value — to access an internal object such as a database record, file, or API resource, without verifying that the requesting user is authorized to access that specific object.
IDOR (Insecure Direct Object Reference) is the general web vulnerability; BOLA (Broken Object Level Authorization) is its API-specific equivalent, holding position API1:2023 in the OWASP API Security Top 10. Both describe the same root cause: missing server-side ownership validation on object-level access.
Yes. IDOR falls under OWASP A01:2025 — Broken Access Control, which holds the #1 position. OWASP documented 1,839,701 occurrences across contributing datasets and found that 100% of tested applications had some form of broken access control.
DAST tools miss most IDOR because detection requires cross-user perspective — two distinct authenticated sessions testing each other's resources. Semgrep's 2025 research found that the best LLM achieves only 22% true positive rate on IDOR detection. Manual testing with two accounts remains the gold standard.
No. UUID v4 (random) prevents enumeration-based IDOR but does not prevent access once an attacker legitimately obtains a reference via a shared link, API response, or log. UUID v1 (timestamp-based) is partially predictable via sandwich attacks (CVE-2024-45719). Server-side ownership checks are always required.
The Optus breach (2022) — a classic IDOR on /api/customers/{id} — exposed 9.8 million records and resulted in an AUD $1.36 million regulatory fine. The McDonald's McHire breach (July 2025) exposed 64 million applicants. Average IDOR remediation costs approximately $25,000 (bounty + engineering).
IDOR alone exposes data. Chained attacks make it critical: IDOR to read a reset token → account takeover; IDOR + mass assignment to write role=admin → privilege escalation; IDOR to read a session cookie → session hijack. The chain, not the initial read, drives critical severity ratings.
In multi-tenant architectures, an IDOR that crosses tenant boundaries exposes an entire organization's dataset rather than a single record. CVSS scores consistently reach 8.8–9.5 for cross-tenant IDOR. Tenant context must be derived from the authenticated session, never from request parameters.