Insecure Direct Object Reference (IDOR)
Insecure Direct Object Reference (IDOR) is a type of access control vulnerability that occurs when an application uses user-supplied input to directly access objects. Attackers can manipulate these references to access unauthorized data—viewing other users' files, modifying their profiles, or accessing sensitive information.
What is IDOR?
IDOR vulnerabilities arise when applications expose internal implementation objects (database records, files, directories) through user-controllable parameters and fail to verify authorization. It's ranked under OWASP A01:2021 Broken Access Control.
Common locations where IDOR occurs:
User profile URLs: /user/123/profileFile downloads: /download?file=invoice_123.pdfAPI endpoints: /api/orders/456Form parameters: <input name="user_id" value="789">
Types of IDOR Attacks
Horizontal Privilege Escalation
Accessing resources belonging to other users at the same privilege level:
# User 123 is logged in and views their own order
GET /api/orders/1001
# Attacker changes the order ID to view another user's order
GET /api/orders/1002 → Returns order belonging to User 456!
# The server doesn't check if User 123 owns Order 1002Vertical Privilege Escalation
Accessing resources meant for higher-privileged users:
# Regular user views their profile
GET /api/users/123?role=user
# Attacker modifies parameter to access admin functionality
GET /api/users/1?role=admin → Returns admin user data!
# Or accessing admin endpoints directly
GET /admin/users/list → No authorization check!Vulnerable Code Examples
Vulnerable API Endpoint
// VULNERABLE: No ownership verification
app.get('/api/documents/:id', async (req, res) => {
const documentId = req.params.id;
// Fetches document without checking who owns it!
const document = await db.documents.findById(documentId);
if (!document) {
return res.status(404).json({ error: 'Not found' });
}
// Returns document to ANY authenticated user
res.json(document);
});
// Attacker can enumerate: /api/documents/1, /api/documents/2, ...Vulnerable File Download
// VULNERABLE: User-controlled filename
app.get('/download', (req, res) => {
const filename = req.query.file;
// No check if user is allowed to access this file!
const filepath = path.join('/uploads', filename);
res.download(filepath);
});
// Attacker requests:
// /download?file=user456_bank_statement.pdf
// /download?file=../../../etc/passwd (Path traversal combo!)Vulnerable Form Processing
// VULNERABLE: Trusting hidden form field
app.post('/profile/update', (req, res) => {
const userId = req.body.user_id; // From hidden form field!
const newEmail = req.body.email;
// Updates whatever user_id is provided
db.users.update(userId, { email: newEmail });
res.send('Profile updated');
});
// Attacker modifies hidden field in browser DevTools:
// <input type="hidden" name="user_id" value="1"> // Admin!Prevention Strategies
1. Always Verify Ownership
// SECURE: Check ownership before access
app.get('/api/documents/:id', async (req, res) => {
const documentId = req.params.id;
const userId = req.session.userId; // From authenticated session
const document = await db.documents.findById(documentId);
if (!document) {
return res.status(404).json({ error: 'Not found' });
}
// Verify the logged-in user owns this document
if (document.ownerId !== userId) {
return res.status(403).json({ error: 'Access denied' });
}
res.json(document);
});2. Use Session-Based User ID
// SECURE: Get user ID from session, not request
app.post('/profile/update', (req, res) => {
// NEVER trust user-supplied user ID
const userId = req.session.userId; // From server-side session!
const newEmail = req.body.email;
db.users.update(userId, { email: newEmail });
res.send('Profile updated');
});
// Even if attacker modifies request, they can only update their own profile3. Query with User Context
// SECURE: Include user context in database query
app.get('/api/orders/:id', async (req, res) => {
const orderId = req.params.id;
const userId = req.session.userId;
// Query includes both order ID AND user ID
const order = await db.orders.findOne({
_id: orderId,
userId: userId // Can only find orders belonging to this user
});
if (!order) {
// Returns 404 whether order doesn't exist OR belongs to someone else
// This prevents information disclosure about valid order IDs
return res.status(404).json({ error: 'Order not found' });
}
res.json(order);
});4. Use Indirect References
// SECURE: Map user-specific index to actual ID
app.get('/api/documents/:index', async (req, res) => {
const index = parseInt(req.params.index);
const userId = req.session.userId;
// Get user's documents and access by index
const userDocs = await db.documents.find({ ownerId: userId });
if (index < 0 || index >= userDocs.length) {
return res.status(404).json({ error: 'Not found' });
}
// User can only access their own documents via index
res.json(userDocs[index]);
});
// User sees /api/documents/0, /api/documents/1 (their docs)
// Can't guess other users' document IDs5. Use UUIDs Instead of Sequential IDs
import { randomUUID } from 'crypto';
// Defense in depth: Use unpredictable IDs
const document = await db.documents.create({
_id: randomUUID(), // e.g., '550e8400-e29b-41d4-a716-446655440000'
ownerId: userId,
content: content
});
// Still implement access control! UUIDs alone aren't sufficient
// They just make enumeration harderTesting for IDOR
IDOR can't be found by automated scanners. Manual testing is required:
- Create two test accounts (User A and User B)
- As User A, create resources (orders, documents, etc.)
- Note the IDs/references used in URLs and parameters
- Log in as User B and try accessing User A's resources
- If successful, IDOR vulnerability confirmed
Security Checklist
- Implement server-side access control for every resource
- Get user identity from server session, not client input
- Include user context in database queries
- Use indirect object references where possible
- Use UUIDs instead of sequential IDs (defense in depth)
- Never expose user IDs in URLs if avoidable
- Return consistent error responses (don't leak info)
- Perform manual penetration testing for IDOR
Practice Challenges
View allDirect Access
Your invoice viewer. But whose invoices can you actually see?
UUID IDOR
Using UUIDs doesn't prevent IDOR if they're guessable.
Horizontal Privilege
Same role, but accessing other users' data.
Vertical Privilege
Regular user accessing admin functions.
Broken Function Auth
UI hides buttons, but API doesn't check permissions.