Skip to content
Fast-turnaround security assessments available — 10+ years development & security experienceGet started
Back to Knowledge Base
vulnerabilityCWE-639OWASP A01:2021Typical severity: Critical

Insecure Direct Object References: The Most Common Access Control Flaw

·9 min read

Insecure Direct Object References: The Most Common Access Control Flaw

Broken access control sits at the top of the OWASP Top 10, and insecure direct object references are the single most common way it manifests. IDOR vulnerabilities are deceptively simple — an application uses a user-supplied identifier to look up a resource without checking whether the requesting user should have access to it. Change the number in the URL, and you are looking at someone else's data.

Despite being well-understood for decades, IDOR remains pervasive. It appears in everything from startup MVPs to enterprise platforms handling millions of records. The reason is structural: building features is faster than building authorization, and the vulnerability is invisible in normal usage. Everything works perfectly when users only access their own resources. The flaw only reveals itself when someone deliberately requests a resource that does not belong to them.

How IDOR Works

Every web application stores data in objects — user profiles, orders, documents, messages, invoices. To retrieve a specific object, the application needs an identifier. When that identifier comes from the client (a URL parameter, a form field, a request body, or even a cookie) and the server fetches the object without verifying ownership or permissions, an IDOR vulnerability exists.

The simplest example:

GET /api/users/1042/profile
Authorization: Bearer <token_for_user_1042>
→ 200 OK (user 1042's profile)

GET /api/users/1043/profile
Authorization: Bearer <token_for_user_1042>
→ 200 OK (user 1043's profile — IDOR)

The server authenticated user 1042 and confirmed they are a valid user. But it never checked whether user 1042 is authorized to view user 1043's profile. Authentication (who are you?) succeeded, but authorization (what are you allowed to do?) was never performed.

Sequential Identifiers Make It Trivial

When applications use auto-incrementing database IDs, IDOR exploitation becomes trivially automated. An attacker writes a simple loop:

for id in range(1, 100000):
    response = requests.get(f'/api/invoices/{id}', headers=auth_headers)
    if response.status_code == 200:
        save(response.json())

In minutes, an attacker can exfiltrate the entire dataset. Sequential IDs also leak business intelligence — the ID value reveals how many objects exist, growth rates, and temporal patterns.

Horizontal vs. Vertical Privilege Escalation

Horizontal privilege escalation occurs when an attacker accesses resources belonging to another user with the same privilege level. User A reads User B's medical records. This is the classic IDOR scenario.

Vertical privilege escalation occurs when an attacker accesses resources or functions restricted to a higher privilege level. A regular user accesses admin endpoints by changing a role parameter or accessing an admin-only resource identifier.

Both types frequently coexist. An application that fails to check resource ownership often also fails to check role-based permissions.

Real-World Impact

Mass Data Exfiltration from a Financial Platform

During a penetration test of a fintech application, testers discovered that the API endpoint for retrieving transaction history accepted an account ID parameter:

GET /api/accounts/ACC-00847291/transactions

The account ID was a sequential identifier with a predictable prefix. By iterating through account numbers, the testers accessed transaction histories for every customer on the platform. Each response contained full transaction details: amounts, dates, counterparty names, and partial account numbers. The entire customer database — over 200,000 accounts — was accessible to any authenticated user.

The business impact was severe. Beyond the obvious privacy violation, the exposed data included enough detail to enable targeted social engineering attacks and financial fraud.

Medical Record Access Through Document References

A healthcare scheduling platform stored medical documents with sequential document IDs. The download endpoint:

GET /api/documents/download?docId=58291

By incrementing the document ID, testers accessed lab results, prescriptions, imaging reports, and clinical notes belonging to other patients. The application verified that the requester was an authenticated user but never checked whether the document belonged to a patient they were authorized to view.

The vulnerability exposed protected health information for tens of thousands of patients. Under healthcare data protection regulations, this represented a reportable breach from the moment the vulnerability was exploitable, regardless of whether anyone had actually exploited it.

Administrative Function Access

An e-commerce platform's admin panel was accessible through a separate URL path, but the API endpoints it called were the same ones available to regular users — just with different parameters. A regular user discovered that sending:

POST /api/orders/12345/refund

Processed the refund without checking whether the requesting user had administrative privileges. The authorization check existed only in the frontend — the admin UI showed the refund button, and the regular user UI did not. But the API itself performed no role verification.

Common IDOR Patterns

Direct Parameter Manipulation

The most straightforward pattern. A resource identifier in the URL path, query string, or request body is changed to reference a different resource.

/api/users/{userId}/settings
/api/orders?orderId={orderId}
/api/download?file={filename}

Body Parameter Tampering

IDOR is not limited to URL parameters. Request bodies in POST and PUT requests often contain resource identifiers that can be manipulated:

json
{
  "userId": 1043,
  "newEmail": "attacker@evil.com"
}

If the server updates the email for whatever userId is provided in the body rather than the authenticated user's ID, the attacker can modify any account.

Sometimes the IDOR is not on the primary object but on a related one. An attacker cannot directly access another user's profile but can access their messages, notifications, or files through a secondary identifier:

GET /api/conversations/7821/messages

The application checks that user 1042 belongs to conversation 7821 but does not check that conversation 7821 belongs to user 1042. If conversation IDs are guessable, the attacker reads private conversations.

Batch and Bulk Operations

APIs that accept arrays of identifiers are particularly dangerous:

json
POST /api/documents/bulk-download
{
  "documentIds": [1001, 1002, 1003, 5847, 5848, 5849]
}

Even if individual document access is protected, bulk operations sometimes skip per-item authorization checks for performance reasons.

File-Based IDOR

When applications store files with predictable names or paths:

/uploads/invoices/2026/user_1042_invoice_march.pdf
/uploads/invoices/2026/user_1043_invoice_march.pdf

Path prediction allows accessing other users' files, even without a database-backed identifier.

Prevention Strategies

Server-Side Authorization on Every Request

The only reliable defense against IDOR is verifying authorization on every request that accesses a resource. This check must happen on the server — never trust client-side enforcement.

python
# WRONG - fetches whatever ID is requested
@app.get("/api/invoices/{invoice_id}")
def get_invoice(invoice_id: int, user: User):
    return db.query(Invoice).filter(Invoice.id == invoice_id).first()
 
# RIGHT - scopes query to the authenticated user
@app.get("/api/invoices/{invoice_id}")
def get_invoice(invoice_id: int, user: User):
    invoice = db.query(Invoice).filter(
        Invoice.id == invoice_id,
        Invoice.owner_id == user.id
    ).first()
    if not invoice:
        raise HTTPException(status_code=404)
    return invoice

Note the use of 404 rather than 403. Returning 403 Forbidden confirms that the resource exists, which is an information leak. Returning 404 Not Found reveals nothing.

Use Indirect References

Instead of exposing database IDs to the client, use per-session indirect reference maps. The server maintains a mapping between the identifiers shown to the user and the actual database IDs, scoped to the current user's session.

Alternatively, derive the resource from the authenticated user's context rather than accepting it as a parameter:

python
# Instead of: GET /api/users/{userId}/profile
# Use: GET /api/me/profile (always returns the authenticated user's profile)

This eliminates the attack surface entirely for self-referential operations.

UUIDs Are Not a Fix (But They Help)

Replacing sequential integers with UUIDs (e.g., 550e8400-e29b-41d4-a716-446655440000) makes identifiers unpredictable, which raises the bar for exploitation. However, UUIDs are not a security control:

  • UUIDs appear in URLs, which are logged, cached, and shared
  • API responses often include UUIDs for related objects
  • Browser history, referrer headers, and link sharing all leak UUIDs
  • A single leaked UUID fully compromises that resource

Use UUIDs as a defense-in-depth measure alongside proper authorization, never as a replacement for it.

Resource-Level Policies

Implement authorization as a policy layer that is consistently applied across all endpoints. Frameworks and libraries like CASL, Casbin, or OPA (Open Policy Agent) allow defining authorization rules declaratively:

# Policy: Users can only read invoices they own
allow(user, "read", invoice) if
    invoice.owner_id == user.id;

# Policy: Admins can read any invoice
allow(user, "read", invoice) if
    user.role == "admin";

This centralizes authorization logic and prevents the inconsistencies that arise when each endpoint implements its own checks.

Enforce Authorization in the Data Layer

The most robust approach is to enforce authorization at the data access layer so that it is impossible to query for resources without scoping to the authorized user:

python
class InvoiceRepository:
    def __init__(self, current_user: User):
        self.user = current_user
 
    def get(self, invoice_id: str) -> Invoice:
        # Authorization is built into every query
        return db.query(Invoice).filter(
            Invoice.id == invoice_id,
            Invoice.owner_id == self.user.id
        ).first()

When authorization is enforced at this level, individual endpoint handlers cannot accidentally skip it.

Testing for IDOR

Systematic IDOR testing requires two authenticated sessions — typically two regular user accounts. For every endpoint that accepts a resource identifier:

  1. Authenticate as User A and note the identifiers in responses (IDs, UUIDs, filenames)
  2. Authenticate as User B and attempt to access User A's resources using those identifiers
  3. Test all HTTP methods — an endpoint might enforce authorization on GET but not on PUT or DELETE
  4. Test related objects — if you cannot access a user's profile, try their orders, messages, or uploaded files
  5. Test batch endpoints — mix authorized and unauthorized identifiers in bulk requests
  6. Test state-changing operations — can you modify or delete another user's resources?
  7. Check numeric, UUID, and slug-based identifiers — IDOR exists regardless of identifier format

Automated scanning has limited effectiveness against IDOR because the scanner needs application-level context to understand which resources belong to which user. Manual testing with business logic understanding remains essential.

Key Takeaways

IDOR vulnerabilities persist because they are invisible during normal development and testing. When developers test their own features, they naturally access their own resources, and everything works. The flaw only manifests when someone deliberately tries to access resources they should not.

Prevention requires treating authorization as a first-class architectural concern:

  1. Verify authorization on every request that accesses a resource, at the server level
  2. Scope all database queries to the authenticated user's permissions
  3. Use indirect references or derive resource identity from the session where possible
  4. Implement centralized authorization policies rather than per-endpoint checks
  5. Return 404 (not 403) for unauthorized resource access to prevent enumeration
  6. Test with multiple user accounts and verify every endpoint enforces proper access control

The cost of fixing IDOR in design is minimal. The cost of fixing it after a data breach is immeasurable.

Need your application tested for insecure direct object references? Get in touch.

Need your application tested?

We find these vulnerabilities in real applications every day. Get a comprehensive security assessment with detailed remediation.

Request an Assessment
idoraccess-controlauthorizationbroken-access-controlapi-security

Summary

Insecure direct object references occur when an application exposes internal object identifiers without verifying that the requesting user is authorized to access them, enabling attackers to access or modify other users' data by simply changing a parameter.

Key Takeaways

  • 1IDOR vulnerabilities occur when applications use predictable identifiers without verifying authorization on the server side
  • 2Broken access control is the number one category in the OWASP Top 10, and IDOR is its most common manifestation
  • 3Both horizontal privilege escalation (accessing other users' data) and vertical privilege escalation (accessing admin functions) are possible
  • 4Replacing sequential IDs with UUIDs is not a fix — authorization checks are the only reliable defense
  • 5Every API endpoint that accepts a resource identifier must verify that the authenticated user has permission to access that specific resource

Frequently Asked Questions

An IDOR vulnerability exists when an application uses a user-supplied identifier (such as a database ID, filename, or key) to directly access an object without checking whether the requesting user is authorized to access it. An attacker can modify the identifier to access resources belonging to other users.

Attackers identify API endpoints or URLs that contain resource identifiers (e.g., /api/invoices/1042) and systematically change those identifiers to access other records. With sequential numeric IDs, this is trivial — incrementing or decrementing the number reveals other users' data.

No. UUIDs make identifiers harder to guess but do not prevent IDOR. If an attacker obtains a UUID through any means — leaked URLs, shared links, API responses, or browser history — the vulnerability is fully exploitable. Authorization checks on the server side are the only reliable defense.

Implement server-side authorization checks on every request that accesses a resource. Verify that the authenticated user has permission to access the specific resource identified in the request. Use resource-level access policies, avoid exposing internal identifiers where possible, and enforce authorization in middleware or data access layers rather than in individual endpoint handlers.