Insecure Direct Object Reference (IDOR)

Object Level AuthorizationInsecure Object Reference

Insecure Direct Object Reference (IDOR) at a glance

What it is: IDOR occurs when an application exposes a reference to an internal object and fails to check whether the requesting user is authorized to access that object.
Why it happens: Attackers can enumerate or guess identifiers to access or modify other users' data
How to fix: Always enforce object ownership or ACL checks on the server before returning or modifying resources; Scope database queries to the authenticated principal or tenant, and centralize object authorization; Avoid accepting authoritative resource ids from clients to determine actor identity; map actions to the authenticated principal

Overview

Insecure direct object reference happens when an attacker can access objects by supplying identifiers (for example numeric ids, UUIDs, filenames, or keys) without the server verifying that the caller is entitled to the object. The root cause is missing object level authorization. IDOR can be accidental, introduced when developers use simple find-by-id patterns, or when admin features expose id parameters that are later re-used without checks.

sequenceDiagram participant Browser participant App as App Server Browser->>App: GET /files/download?id=101 App-->>Browser: file for id 101 (owner B) Browser->>App: GET /files/download?id=102 App-->>Browser: file for id 102 (owner C) note over Browser: Enumeration harvests many files without ownership check
A potential flow for a Insecure Direct Object Reference (IDOR) exploit

Where it occurs

IDOR commonly appears in RESTful endpoints that accept ids in path or query parameters, file download routes, APIs that accept an id in JSON payloads, and admin or multi-tenant services that fail to scope queries by tenant or owner.

Impact

Impacts range from disclosure of private files and user data to unauthorized changes to other users' accounts. Depending on the resource, consequences include privacy breaches, fraud, data tampering, and compliance violations.

Prevention

Enforce deny by default for object access. Scope queries to the authenticated principal or tenant, use centralized ACL checks or policy engines, and keep high privilege operations behind explicit admin-only endpoints. Where possible, avoid accepting a user supplied id as the authority that identifies the subject; instead derive the subject from the authentication context. Add tests and CI checks that exercise resource access for negative cases.

Examples

Switch tabs to view language/framework variants.

Express, GET /invoices/:id returns any invoice without ownership check

Handler returns invoice by id with no verification that the requesting user owns it.

Vulnerable
JavaScript • Express — Bad
const express = require('express');
const app = express();
app.get('/invoices/:id', async (req, res) => {
  const inv = await db.Invoices.findById(req.params.id);
  if (!inv) return res.status(404).send('not found');
  res.json(inv); // BUG: no owner check
});
  • Line 6: No ownership or tenant check before returning an invoice

Endpoints that accept resource identifiers but do not check that the caller is authorized to access that resource enable IDOR.

Secure
JavaScript • Express — Good
app.get('/invoices/:id', async (req, res) => {
  const userId = req.user.id;
  const inv = await db.Invoices.findOne({ id: req.params.id, ownerId: userId });
  if (!inv) return res.status(404).send('not found');
  res.json(inv);
});
  • Line 1: Scope the DB query to the authenticated principal

Always derive resource scope from the authenticated principal or enforce explicit ACL checks.

Engineer Checklist

  • Scope DB queries by authenticated principal or tenant for object retrieval and modification

  • Centralize object authorization checks at service or data access layer

  • Avoid letting clients supply authoritative user or tenant ids for sensitive operations

  • Treat predictable ids as untrusted; make tests that attempt cross account accesses

  • Log and alert on unusual enumeration patterns and bulk id access

  • Review API designs that accept ids and consider returning opaque tokens or signed urls for file downloads

End-to-End Example

A SaaS app exposes a file download endpoint that accepts a numeric id. Files are stored per user and ids are sequential. The download handler does not verify that the requesting user owns the file. An attacker iterates numeric ids and downloads hundreds of other customers' files.

Vulnerable
JAVASCRIPT
// Node.js/Express - Vulnerable IDOR endpoint

app.get('/api/users/:userId/profile', authenticateToken, async (req, res) => {
  try {
    // VULNERABLE: Uses userId from URL without checking if it matches authenticated user
    const userId = req.params.userId;
    
    // No authorization check - returns ANY user's profile!
    const user = await db.users.findById(userId);
    
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }

    // Exposes sensitive data from ANY user to ANY authenticated user
    res.json({
      id: user.id,
      username: user.username,
      email: user.email,
      role: user.role,
      api_key: user.api_key,
      address: user.address,
      phone: user.phone
    });
  } catch (err) {
    res.status(500).json({ error: 'Server error' });
  }
});

app.get('/api/documents/:docId/download', authenticateToken, async (req, res) => {
  try {
    // VULNERABLE: No ownership check on document access
    const docId = req.params.docId;
    
    const document = await db.documents.findById(docId);
    
    if (!document) {
      return res.status(404).json({ error: 'Document not found' });
    }

    // Sends file to ANY authenticated user, regardless of owner_id!
    res.download(document.filepath, document.filename);
  } catch (err) {
    res.status(500).json({ error: 'Server error' });
  }
});
Secure
JAVASCRIPT
// Node.js/Express - SECURE IDOR-protected endpoint

app.get('/api/users/:userId/profile', authenticateToken, async (req, res) => {
  try {
    const requestedUserId = parseInt(req.params.userId);
    const authenticatedUserId = req.user.id; // From JWT token
    
    // SECURE: Verify requested user matches authenticated user
    if (requestedUserId !== authenticatedUserId) {
      return res.status(403).json({ error: 'Access denied' });
    }
    
    // Now safe to query - can only access own profile
    const user = await db.users.findById(authenticatedUserId);
    
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }

    res.json({
      id: user.id,
      username: user.username,
      email: user.email,
      role: user.role
      // Don't expose api_key in API responses
    });
  } catch (err) {
    res.status(500).json({ error: 'Server error' });
  }
});

app.get('/api/documents/:docId/download', authenticateToken, async (req, res) => {
  try {
    const docId = req.params.docId;
    const userId = req.user.id; // From authenticated token
    
    // SECURE: Scope query by BOTH document ID and owner
    const document = await db.documents.findOne({
      id: docId,
      owner_id: userId  // Only returns if user owns this document
    });
    
    if (!document) {
      // Return 404 for both missing and unauthorized (don't leak existence)
      return res.status(404).json({ error: 'Document not found' });
    }

    // Only sends files that belong to the authenticated user
    res.download(document.filepath, document.filename);
  } catch (err) {
    res.status(500).json({ error: 'Server error' });
  }
});

// ALTERNATIVE: Use opaque tokens instead of predictable IDs
app.get('/api/documents/download/:token', authenticateToken, async (req, res) => {
  // Token is a signed, time-limited URL token (e.g., JWT with docId + expiry)
  const decoded = verifyDownloadToken(req.params.token);
  
  if (!decoded || decoded.userId !== req.user.id) {
    return res.status(403).json({ error: 'Invalid token' });
  }
  
  const document = await db.documents.findById(decoded.docId);
  res.download(document.filepath, document.filename);
});

Discovery

Test if object IDs in URLs can be modified to access other users' resources without authorization checks.

  1. 1. Enumerate user ID pattern

    http

    Action

    Access own profile to identify ID parameter format

    Request

    GET https://api.example.com/api/users/1042/profile
    Headers:
    Authorization: Bearer user-1042-token

    Response

    Status: 200
    Body:
    {
      "id": 1042,
      "username": "alice",
      "email": "alice@example.com",
      "role": "user"
    }

    Artifacts

    id_parameter_identified sequential_ids
  2. 2. Test horizontal privilege escalation

    http

    Action

    Modify ID to access another user's profile

    Request

    GET https://api.example.com/api/users/1/profile
    Headers:
    Authorization: Bearer user-1042-token

    Response

    Status: 200
    Body:
    {
      "id": 1,
      "username": "admin",
      "email": "admin@example.com",
      "role": "admin",
      "api_key": "sk_live_admin_key_xyz789"
    }

    Artifacts

    idor_confirmed admin_account_accessed api_key_disclosed

Exploit steps

Attacker iterates through user IDs to extract all user data, including admin accounts and sensitive information.

  1. 1. Enumerate all user profiles

    Iterate through user IDs

    http

    Action

    Loop through IDs 1-10000 to extract all user data

    Request

    GET https://api.example.com/api/users/{1..10000}/profile
    Headers:
    Authorization: Bearer user-1042-token

    Response

    Status: 200
    Body:
    {
      "note": "Extracted 8,543 user profiles including emails, phone numbers, addresses. Found 15 admin accounts with elevated privileges and API keys."
    }

    Artifacts

    mass_data_exfiltration user_pii admin_accounts api_keys
  2. 2. Access sensitive documents

    Download other users' private files

    http

    Action

    Modify document IDs to access confidential files

    Request

    GET https://api.example.com/api/documents/d-9876/download
    Headers:
    Authorization: Bearer user-1042-token

    Response

    Status: 200
    Body:
    {
      "filename": "Q4_Financial_Results_CONFIDENTIAL.pdf",
      "owner": "CFO (user_id: 5)",
      "content": "Revenue: $45M, Profit: $12M, Customer acquisition cost increased 23%..."
    }

    Artifacts

    confidential_documents financial_data cross_user_access

Specific Impact

Attackers retrieve private customer files leading to data breaches, potential regulatory exposure, and reputational damage. Affected customers may include sensitive personal data or proprietary documents.

Remediation requires patching endpoints to enforce ownership, rotating any exposed secrets, contacting impacted customers, and running audits for similar endpoints.

Fix

Scope database queries by owner id or tenant, return 404 when missing, and consider using signed, time limited URLs for downloads to avoid direct id exposure. Add tests that verify cross account access is denied.

Detect This Vulnerability in Your Code

Sourcery automatically identifies insecure direct object reference (idor) vulnerabilities and many other security issues in your codebase.

Scan Your Code for Free