In 2022, a researcher found that changing a single number in a Trello API URL exposed private board data belonging to other accounts. That is a textbook IDOR. It did not require a complex exploit or bypassing encryption. The app was simply fetching a record by ID without checking who owned it.
TL;DR
IDOR (Insecure Direct Object Reference) happens when your app fetches a resource using an ID from the request without verifying the current user owns it. Change /api/invoices/123 to /api/invoices/124 and you see another user's invoice. The fix is one check: confirm the record's owner ID matches the logged-in user before returning data.
What Does IDOR Stand For?
IDOR is short for Insecure Direct Object Reference. Break it down:
- Direct Object Reference: your app uses a raw database ID (like
123oruuid-abc) to look up a record directly. - Insecure: the app never checks whether the person making the request is allowed to see that record.
The result is that anyone who knows (or guesses) a valid ID can read, modify, or delete records they do not own.
Where IDOR Sits in the OWASP Top 10
IDOR falls under Broken Access Control, which is OWASP's #1 web application security risk. OWASP defines it as any situation where users can act outside their intended permissions. IDOR is the most common concrete pattern inside that category because it requires almost no sophistication to exploit.
Sequential integer IDs make it trivial: if your invoice is at /api/invoices/1052, an attacker tries 1051, 1050, and 1053 and sees who else has accounts.
AI code generators (Cursor, Bolt, Lovable, v0) produce working CRUD endpoints fast but routinely skip ownership checks. The generated code authenticates the request correctly, then forgets to verify the user owns the fetched object. This is the single most common finding in CheckYourVibe scans.
A Concrete Example
Your API endpoint fetches a document by ID:
Vulnerable code
// BAD: fetches document but never checks ownership app.get('/api/documents/:id', async (req, res) => { const document = await db.documents.findById(req.params.id); res.json(document); // any authenticated user sees any document });
User A logs in and loads /api/documents/101. That is their document. They change the URL to /api/documents/102. The server fetches it and returns it because the user is authenticated, even though they do not own document 102.
Fixed code
// GOOD: verify ownership before returning app.get('/api/documents/:id', async (req, res) => { const document = await db.documents.findById(req.params.id);
if (!document || document.userId !== req.user.id) { return res.status(403).json({ error: 'Not authorized' }); }
res.json(document); });
The 5-Minute Fix Pattern
The most reliable pattern is the scoped query: include the current user's ID in the WHERE clause so the database itself enforces ownership. If the record does not belong to the user, the query returns nothing.
Scoped query (recommended)
// Best: the DB only returns records the user owns const document = await db.documents.findOne({ where: { id: req.params.id, userId: req.user.id // row is invisible if ownership fails } });
if (!document) { return res.status(404).json({ error: 'Not found' }); }
This approach has an advantage over a separate ownership check: it avoids a time-of-check/time-of-use race condition and never leaks whether the record exists.
Prevention Strategies
- Scope every query to the user: Add
userId: req.user.idto everyfindOneorfindByIdthat retrieves user-owned data. - Use Row Level Security: Enable RLS in your database so ownership is enforced at the DB layer, even if app code has a bug.
- Use UUIDs: Sequential IDs are trivial to enumerate. UUIDs do not prevent IDOR but raise the cost of blind guessing from trivial to impractical.
- Audit indirect references: APIs that return IDs in responses (order IDs in a receipt email, attachment IDs in a download link) can expose targets for IDOR even if the UI does not show them.
- Test it yourself: After building any GET endpoint, log in as User A, get a valid ID, then log in as User B and request that same ID. If you see User A's data, you have IDOR.
For Supabase projects, Row Level Security is the most robust fix. A single RLS policy on a table protects every query against that table, including ones you forgot to scope in your app code. See the RLS glossary entry for policy examples.
What does IDOR stand for?
IDOR stands for Insecure Direct Object Reference. It means your app directly exposes a database or file ID in a URL or request, and lets users substitute any value for it without checking whether they own that resource.
What is IDOR in security?
IDOR is an access-control vulnerability in the OWASP Top 10 (under Broken Access Control, the #1 web risk). It happens when a server fetches a record by ID without confirming the requesting user is authorized to see it. Attackers increment or guess IDs to access other users' data.
How do I prevent IDOR vulnerabilities?
Always verify that the current user has permission to access the requested resource. Check ownership or permissions in your code, not just authentication. Use indirect references like UUIDs instead of sequential IDs. Implement proper authorization middleware that runs on every request.
Do UUIDs prevent IDOR?
UUIDs make IDOR harder because attackers cannot easily guess valid IDs. However, they do not prevent IDOR entirely. If an attacker learns a valid UUID (through a link, API response, or leak), they can still try to access it. You still need authorization checks.
What is the difference between IDOR and broken authorization?
IDOR is a specific type of broken authorization that involves direct object references. Broken authorization is the broader OWASP category that includes any failure to restrict access. IDOR typically involves changing IDs in URLs or request bodies to reach another user's resources.
Find IDOR Vulnerabilities in Your App
CheckYourVibe scans for missing ownership checks and other access control issues in apps built with AI tools.