Insecure Direct Object Reference (IDOR)
Insecure Direct Object Reference (IDOR) at a glance
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.
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.
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.
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.
// 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' });
}
});// 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. Enumerate user ID pattern
httpAction
Access own profile to identify ID parameter format
Request
GET https://api.example.com/api/users/1042/profileHeaders:Authorization: Bearer user-1042-tokenResponse
Status: 200Body:{ "id": 1042, "username": "alice", "email": "alice@example.com", "role": "user" }Artifacts
id_parameter_identified sequential_ids -
2. Test horizontal privilege escalation
httpAction
Modify ID to access another user's profile
Request
GET https://api.example.com/api/users/1/profileHeaders:Authorization: Bearer user-1042-tokenResponse
Status: 200Body:{ "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. Enumerate all user profiles
Iterate through user IDs
httpAction
Loop through IDs 1-10000 to extract all user data
Request
GET https://api.example.com/api/users/{1..10000}/profileHeaders:Authorization: Bearer user-1042-tokenResponse
Status: 200Body:{ "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. Access sensitive documents
Download other users' private files
httpAction
Modify document IDs to access confidential files
Request
GET https://api.example.com/api/documents/d-9876/downloadHeaders:Authorization: Bearer user-1042-tokenResponse
Status: 200Body:{ "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