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 stands for Insecure Direct Object Reference. Your app exposes a reference to an internal object (like a database record) and doesn't check whether the requesting user is allowed to see it. That's it. Simple in concept, and responsible for a huge share of data breaches in production apps built with AI tools.
Here's what it looks like in practice:
// 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 build functional CRUD operations. Authorization checks, though, get skipped. 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 the happy path. It works just as well for attackers.
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 triggers breach notification obligations and potential fines. "Can result in" isn't the right framing here: it's a hard requirement in most jurisdictions once personal data leaks.
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
Instead of exposing your database ID directly, map a user-specific reference to the real record server-side:
// 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?
Very common. We see it in the majority of AI-built apps we scan. AI tools generate CRUD endpoints that work correctly for the intended user but don't check whether a different user should be blocked. It's not the AI's fault exactly. It's doing what you asked. You have to ask for authorization too.
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.