TL;DR
IDOR happens when your app uses predictable IDs (like /user/123) without checking if the logged-in user should access that resource. Attackers simply change the ID to access other users' data. The fix: always verify the requesting user owns or has permission to access the requested resource before returning it.
What Is IDOR?
IDOR (Insecure Direct Object Reference) is one of the most common vulnerabilities in web applications. It occurs when an application exposes a reference to an internal object (like a database record) and doesn't verify whether the user should have access to that object.
Here's a simple example:
// URL: /api/invoices/1001
// User A is logged in and can see their invoice
// User A changes the URL to: /api/invoices/1002
// Now they can see User B's invoice!
// The server code:
app.get('/api/invoices/:id', async (req, res) => {
const invoice = await db.invoice.findUnique({
where: { id: parseInt(req.params.id) }
});
res.json(invoice); // No check if user owns this invoice!
});
Why IDOR Is So Common in Vibe-Coded Apps
AI code generators create functional CRUD operations but often skip authorization checks. When you ask for "an API to fetch invoices," you get:
// AI-generated code - works but insecure
app.get('/api/invoices/:id', async (req, res) => {
const invoice = await prisma.invoice.findUnique({
where: { id: req.params.id }
});
if (!invoice) return res.status(404).json({ error: 'Not found' });
res.json(invoice);
});
app.delete('/api/invoices/:id', async (req, res) => {
await prisma.invoice.delete({
where: { id: req.params.id }
});
res.json({ success: true });
});
The code works perfectly for its intended use case. The problem is it works for attackers too.
Common IDOR Patterns
1. Sequential IDs in URLs
/api/orders/1001 → /api/orders/1002 → /api/orders/1003
/users/profile/42 → /users/profile/43 → /users/profile/44
/documents/download/1 → /documents/download/2
2. Hidden Form Fields
<form action="/api/update-profile" method="POST">
<input type="hidden" name="userId" value="123" />
<!-- Attacker changes this to another user's ID -->
</form>
3. API Parameters
// Original request from app:
POST /api/transfer
{ "fromAccount": "user123", "toAccount": "shop", "amount": 50 }
// Attacker modifies:
POST /api/transfer
{ "fromAccount": "otherUser456", "toAccount": "attacker", "amount": 5000 }
Real Consequences of IDOR
| Endpoint Type | IDOR Consequence |
|---|---|
| User profiles | View private information, emails, addresses |
| Financial records | Access invoices, payment history, account balances |
| Documents/files | Download confidential documents |
| Delete operations | Delete other users' data |
| Admin functions | Access admin panels, modify settings |
GDPR impact: IDOR that exposes personal data can result in data breach notification requirements and significant fines under privacy regulations.
How to Fix IDOR
1. Always Verify Ownership
// SECURE: Check user owns the resource
app.get('/api/invoices/:id', authenticate, async (req, res) => {
const invoice = await prisma.invoice.findUnique({
where: {
id: req.params.id,
userId: req.user.id // Only return if user owns it
}
});
if (!invoice) {
return res.status(404).json({ error: 'Not found' });
}
res.json(invoice);
});
2. Use Session Data for User Identity
// WRONG: User ID from request body
app.post('/api/update-profile', async (req, res) => {
await updateUser(req.body.userId, req.body.data); // Vulnerable!
});
// CORRECT: User ID from authenticated session
app.post('/api/update-profile', authenticate, async (req, res) => {
await updateUser(req.user.id, req.body.data); // Secure!
});
3. Use Indirect References
Map public-facing IDs to user-specific references:
// Instead of /api/documents/42 (database ID)
// Use /api/documents/doc_a3f7 (user-specific reference)
// User A's documents: doc_a1, doc_a2, doc_a3
// User B's documents: doc_b1, doc_b2, doc_b3
// These map to different database IDs for each user
app.get('/api/documents/:ref', authenticate, async (req, res) => {
const doc = await getDocumentByUserRef(req.user.id, req.params.ref);
if (!doc) return res.status(404).json({ error: 'Not found' });
res.json(doc);
});
4. Implement Row-Level Security
For Supabase or PostgreSQL, use RLS to enforce access at the database level:
-- Users can only see their own invoices
CREATE POLICY "Users see own invoices" ON invoices
FOR SELECT USING (auth.uid() = user_id);
-- Users can only update their own invoices
CREATE POLICY "Users update own invoices" ON invoices
FOR UPDATE USING (auth.uid() = user_id);
Defense in depth: Even with RLS, implement authorization checks in your application code. Multiple layers of protection ensure security even if one layer fails.
What is IDOR?
IDOR (Insecure Direct Object Reference) is when an application exposes internal object identifiers (like database IDs) without proper authorization checks. Attackers can access other users' data by simply changing ID values in URLs or API requests.
Does using UUIDs prevent IDOR?
UUIDs make IDOR harder to exploit because they're not sequential, but they don't prevent it. Attackers might find UUIDs through other parts of your application. Always implement proper authorization checks regardless of ID format.
How common is IDOR in AI-generated code?
IDOR is extremely common in AI-generated code. AI tools often generate functional CRUD endpoints without proper authorization checks. The code works correctly for the happy path but doesn't verify users can only access their own data.
How do I test for IDOR?
Create two test accounts. Log in as user A, access a resource, then change the ID to a resource belonging to user B. If you can access user B's data, you have an IDOR vulnerability. Test all endpoints that use IDs.
Find IDOR Vulnerabilities
Our scanner tests your API endpoints for authorization issues.
Start Free Scan