In May 2019, a real estate developer noticed that a document link from First American Financial pointed at a nine-digit record number. Changing one digit pulled up someone else's file. KrebsOnSecurity reported that roughly 885 million title and escrow records, going back to 2003, were reachable that way: bank account numbers, Social Security numbers, wire receipts.
No exploit. No stolen password. The site fetched a document by its number and never asked who was asking. That's IDOR. It also wasn't a secret: when the SEC settled with the company in June 2021 (a $487,616 penalty), it found First American's own security staff had flagged the flaw months before the story ran.
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 used to have its own slot. It was A4, "Insecure Direct Object References", in OWASP's 2013 Top 10. In 2017 OWASP merged it with "Missing Function Level Access Control" into a single category, Broken Access Control. That category climbed to A01, the top spot, in the 2021 list and held it in the 2025 list.
So if you search the current Top 10 for "IDOR" you won't find a heading. It's the most familiar concrete pattern inside A01, because exploiting it takes a browser and some patience, nothing more. OWASP also keeps a dedicated IDOR Prevention Cheat Sheet.
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. Logged in is not the same as allowed, and the code only checks the first one.
A Concrete Example
Your API endpoint fetches a document by ID:
// 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.
// 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.
// 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' });
}
It has one more advantage over the fetch-then-check version: every miss returns the same 404. The 403 in the earlier example tells an attacker "this ID exists, it just isn't yours", which is enough to map how many records you have.
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. OWASP listed it as its own Top 10 category until 2017, when it was folded into Broken Access Control, which is A01 (the top risk) in both the 2021 and 2025 lists. It happens when a server fetches a record by ID without confirming the requesting user is allowed to see it, so attackers change or guess IDs to reach 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
An ownership check lives in your server code, so the reliable test is the two-account one above. CheckYourVibe covers the exposure around it from the outside: API keys leaked into your JavaScript, exposed .env files and admin paths, permissive CORS, and missing security headers.