IDOR: The Vulnerability Hiding in Your URLs 🔓
IDOR: The Vulnerability Hiding in Your URLs 🔓
Let me paint you a picture. You're using a web app, you check your invoice at /invoices/1042, and out of curiosity you change that number to /invoices/1041. Boom — you're looking at someone else's invoice. Name, address, order details. All of it.
That's an IDOR. And I promise you, it's somewhere in a codebase you've written.
What Even Is IDOR? 🤔
IDOR stands for Insecure Direct Object Reference. It's a classic from the OWASP Top 10, and the concept is brutally simple:
Your app exposes a reference to an internal object (a database ID, a filename, a UUID) and doesn't check if the person requesting it is actually allowed to see it.
That's it. The whole thing. No black magic, no buffer overflows. Just missing authorization checks.
The reason it's so common? Because developers think about authentication ("is this person logged in?") but forget about authorization ("is this person allowed to access this specific thing?").
The Classic Example 💀
Here's vulnerable Node.js code you've probably written (or reviewed) before:
// ❌ VULNERABLE - No authorization check!
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await Order.findById(req.params.id);
if (!order) {
return res.status(404).json({ error: 'Not found' });
}
// We checked if the user is logged in (authenticate middleware)
// but NEVER checked if this order belongs to them!
res.json(order);
});
Any authenticated user can grab any order by cycling through IDs. If you're using auto-incrementing integers, it's even worse — an attacker can enumerate every order in your database with a simple script.
The Fix Is Embarrassingly Simple ✅
// ✅ SECURE - Always scope queries to the current user
app.get('/api/orders/:id', authenticate, async (req, res) => {
const order = await Order.findOne({
_id: req.params.id,
userId: req.user.id // 👈 This one line changes everything
});
if (!order) {
// Returns 404 for both "not found" AND "not yours"
// Don't leak the difference!
return res.status(404).json({ error: 'Not found' });
}
res.json(order);
});
By scoping the database query to the authenticated user's ID, you guarantee that even if someone guesses another order's ID, they get a 404. Clean. Simple. Secure.
It's Not Just IDs — It's Everywhere 🕵️
IDOR hides in more places than just numeric IDs in URLs. Watch out for:
- File downloads:
/download?file=report_2024_user_42.pdf - Account settings:
PUT /api/users/99/email(can user 100 change user 99's email?) - UUIDs: They're harder to guess but NOT a security control. Obscurity ≠ security.
- Indirect references:
/api/messages?conversation=abc123— who owns that conversation? - Admin endpoints:
/admin/users/55/disable— is the caller actually an admin?
UUIDs deserve special attention because a lot of teams switch from integer IDs to UUIDs and call it a day. A UUID is 36 characters of "harder to guess" — but if it ever leaks in a log, a URL share, or an API response, it's game over.
A Real-World Scale Example 😬
In 2019, a major fintech platform had an IDOR in their document download endpoint. Users could download any KYC document (passports, utility bills, bank statements) by changing a numeric ID in the request. Hundreds of thousands of sensitive documents were exposed.
The fix? Two lines of code. A WHERE user_id = $current_user clause.
The cost of NOT having those two lines? Regulatory fines, breach notifications, and a PR nightmare that lasted months.
How to Hunt for IDOR in Your Own App 🔍
Here's a quick checklist to run through your codebase:
# Grep for common patterns where authorization might be missing
# Look for any route that takes an ID parameter
grep -rn "req.params.id\|req.query.id\|params\[:id\]" ./src
# For each hit, ask yourself:
# 1. Does this query scope to the current user?
# 2. Can an authenticated-but-wrong user reach this data?
# 3. Am I returning different errors for "not found" vs "not yours"?
The third point matters more than people realize. If your API returns 403 Forbidden for unauthorized access but 404 Not Found for missing resources, attackers can use that difference to confirm whether a record EXISTS before figuring out how to access it. Always return 404 for both.
The Horizontal vs Vertical IDOR Distinction
Security folks split these into two flavors:
- Horizontal IDOR: Same privilege level, different user. User A reads User B's data. This is the classic case above.
- Vertical IDOR: Lower privilege escalates to higher privilege. A regular user accesses an admin-only resource by guessing an admin object ID.
Both are equally bad. Vertical IDORs are often more catastrophic because they can lead to full account takeover or system compromise.
Quick Defense Checklist ✅
Before you ship any feature that fetches data by ID:
- Does every query include
AND owner_id = current_user.id(or equivalent)? - For role-based resources, does every query verify the caller's role?
- Are you returning the same error (404) for both "not found" and "unauthorized"?
- Are you logging access to sensitive resources so you can detect enumeration attacks?
- Have you tested with two different accounts — can Account B access Account A's data?
That last one is the simplest and most effective test. Create two test users, generate data with one, and try to access it with the other. If it works, you have an IDOR.
The Mental Model Shift 🧠
Stop thinking about authorization as a gate you pass through once (login). Think of it as a property of every single data access in your system.
The question isn't "is this user authenticated?" The question is "does this user have permission to access this specific record?"
Every. Single. Query.
When you internalize that distinction, IDORs become nearly impossible to accidentally introduce.
Spotted an IDOR somewhere interesting? Report it responsibly and you might score a bug bounty. Hit me up on LinkedIn — I love hearing disclosure stories.
Want more security content? Follow the blog! We cover the vulnerabilities that actually show up in real production code, not just theoretical textbook stuff. Communities like YAS and InitCrew are also great places to sharpen your security instincts. 🔐
Now go audit your /api/:id routes. I'll wait. 🕵️✨