IDOR: The Vulnerability Hiding in Plain Sight ๐๏ธ
IDOR: The Vulnerability Hiding in Plain Sight ๐๏ธ
Imagine you book a flight online, get your confirmation email, and notice the URL looks something like this:
https://airline.com/booking?id=38291
You're curious. You change the 38291 to 38290. And suddenly you're staring at someone else's full booking โ their name, their passport number, their travel plans.
Congratulations, you've just discovered an IDOR โ Insecure Direct Object Reference. And you've probably also just discovered that this airline has some serious explaining to do. โ๏ธ
So What Exactly Is an IDOR? ๐ค
IDOR is OWASP's bread-and-butter finding. It happens when your app exposes a reference to an internal object (a database ID, a filename, an order number) and then trusts the user not to tamper with it โ without checking whether they're actually allowed to access that object.
No fancy hacking tools. No buffer overflows. Just... changing a number in a URL or a request body.
It's the vulnerability equivalent of a locked door where someone forgot to install the lock.
The Classic Example (That's Depressingly Common) ๐ฌ
Here's a typical Express.js endpoint that ships every single day:
// โ The "we're going to get breached" version
app.get('/api/orders/:id', authenticateToken, async (req, res) => {
const order = await Order.findById(req.params.id);
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}
// We checked the token... but not WHO owns this order ๐คฆ
res.json(order);
});
The dev here did something right โ they require authentication. But they forgot the second question: does this authenticated user actually own this resource?
An attacker logs into their account, grabs a valid token, then loops through IDs:
# Attacker's script (embarrassingly simple)
for id in $(seq 1 10000); do
curl -H "Authorization: Bearer $VALID_TOKEN" \
https://yourapi.com/api/orders/$id
done
And your entire order history is now in a CSV on their desktop. ๐
The Fix: One Line of Code Between You and a Data Breach โ
// โ
The "sleep soundly at night" version
app.get('/api/orders/:id', authenticateToken, async (req, res) => {
const order = await Order.findOne({
_id: req.params.id,
userId: req.user.id // โ This one line is everything
});
if (!order) {
// Return 404 (not 403) โ don't confirm the resource exists
return res.status(404).json({ error: 'Order not found' });
}
res.json(order);
});
The magic is userId: req.user.id. We're not just finding the order โ we're finding the order that belongs to this specific user. If the ID exists but belongs to someone else, it returns null, same as not found.
Bonus security tip: Return 404 instead of 403 for unauthorized resources. A 403 tells the attacker "this exists, you just can't have it." A 404 tells them nothing useful.
Where IDOR Hides (Beyond the Obvious URL) ๐ต๏ธ
Developers often fix the obvious cases and miss the sneaky ones. IDOR lurks in:
Request bodies:
POST /api/profile/update
{ "userId": 99, "email": "[email protected]" }
Does your backend use the userId from the request... or from the auth token? If it's the former, anyone can update anyone's profile.
File downloads:
GET /uploads/invoice_38291.pdf
Static file paths with predictable names are IDOR goldmines. A sequential invoice number or a UUID-v1 (which is time-based and guessable) gives attackers a roadmap.
GraphQL queries:
query {
user(id: "550e8400-e29b-41d4-a716-446655440000") {
email
phoneNumber
address
}
}
GraphQL feels fancy and safe. It isn't. Every object query needs the same authorization checks as your REST endpoints.
The Real-World Damage ๐ธ
IDOR has been behind some brutal breaches:
- Venmo (2018): Every transaction was public by default. With the API, you could scrape anyone's payment history โ politicians, celebrities, drug dealers. The data was used in journalism exposรฉs.
- Facebook (2020): IDOR in their Business Suite allowed attackers to read messages from any Facebook page.
- Australian Government myGov (2020): IDOR let anyone access other citizens' linked welfare records. In a government healthcare app.
These aren't obscure edge cases. They're the kind of "oops" that ends careers and triggers congressional hearings.
Write a Test So You Never Ship This Again ๐งช
The best time to catch IDOR is before production. Here's a dead-simple test pattern:
// Jest + Supertest example
describe('Order API - Authorization', () => {
it('should not allow user B to access user A orders', async () => {
// Create two separate users with their own orders
const userA = await createTestUser('[email protected]');
const userB = await createTestUser('[email protected]');
const orderA = await createTestOrder(userA.id);
// User B tries to access User A's order
const response = await request(app)
.get(`/api/orders/${orderA.id}`)
.set('Authorization', `Bearer ${userB.token}`);
// Should NOT succeed
expect(response.status).toBe(404);
expect(response.body).not.toHaveProperty('creditCard');
});
});
Make this pattern a habit. For every resource endpoint, ask: "What happens when a different authenticated user hits this?"
If the answer is "they get the data anyway," you have an IDOR.
The Developer Mindset Shift ๐ง
The root cause of IDOR is trusting client-supplied identifiers without re-validating ownership. The fix is a mindset:
Never ask "does this resource exist?" โ always ask "does this resource exist AND belong to this user?"
A few architectural patterns that help:
- Always scope queries to the authenticated user.
WHERE id = ? AND user_id = ?should be muscle memory. - Use UUIDs instead of sequential IDs. They're harder to enumerate โ though not a substitute for real authorization.
- Centralize authorization logic. Don't scatter ownership checks across 50 controllers. Use middleware or a dedicated authorization layer.
- Log access patterns. Hundreds of 404s from one user hitting sequential IDs is a very loud alarm bell.
Your IDOR Checklist Before Every Deploy ๐
- Every resource endpoint scopes queries to the authenticated user
- File downloads verify ownership before serving
- GraphQL resolvers have authorization checks, not just field-level ones
- Request bodies don't accept
userId/accountIdfrom the client - Cross-user access tests exist for every resource type
- Sequential IDs are not the only protection on sensitive endpoints
Found an IDOR in someone's app? Responsible disclosure is the move โ check if they have a bug bounty program. Many companies pay good money for exactly this.
Found an IDOR in your own app? Fix it, rotate any exposed data, and add that test. You've just made your users safer.
Hit me up on LinkedIn if you've got an IDOR war story โ the good, the bad, and the "I can't believe that was in prod." ๐
P.S. โ If you're using sequential integer IDs for user-facing resources and have no ownership checks, please close this tab and fix that first. I'll be here. ๐ก๏ธ