IDOR — Insecure Direct Object Reference — consistently ranks as one of the most common and most impactful vulnerabilities found in web applications. It’s also one of the simplest. No complex payloads. No chaining. Just changing a number in a request.
This code review covers a classic example — a profile endpoint that forgets to ask one critical question.
The Code
app.get('/api/user/profile', async (req, res) => {
const userId = req.query.userId;
const user = await db.query(
'SELECT id, name, email, phone, address FROM users WHERE id = ?',
[userId]
);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
return res.json(user);
});
Simple. Clean. Broken.
Breaking It Down
The endpoint accepts a userId parameter from the query string and fetches that user’s profile from the database. The response includes name, email, phone number, and home address.
Here’s the flow:
- Request comes in with
?userId=123 userIdis taken directly fromreq.query- A database query fetches the user with that ID
- The full profile is returned — name, email, phone, address
At no point does the code ask: “Does the person making this request actually own this profile?”
That missing question is the entire vulnerability.
The Attack
An authenticated user makes a legitimate request to view their own profile:
GET /api/user/profile?userId=1001 HTTP/1.1
Authorization: Bearer <valid_token>
Response:
{
"id": 1001,
"name": "Current User",
"email": "user@email.com",
"phone": "555-0101",
"address": "123 Main St"
}
Now they change the userId to someone else’s:
GET /api/user/profile?userId=1002 HTTP/1.1
Authorization: Bearer <valid_token>
Response:
{
"id": 1002,
"name": "Another User",
"email": "victim@email.com",
"phone": "555-0199",
"address": "456 Private Rd"
}
Full PII of any user in the system — returned without restriction. The attacker just needs to increment the ID and repeat. Automated with a script, this becomes a full database dump of every user’s personal information.
Why Authentication Alone Is Not Enough
The endpoint does require authentication — you need a valid Bearer token to call it. Many developers stop there and consider the endpoint secure.
Authentication answers: “Who are you?”
Authorization answers: “Are you allowed to do this?”
This endpoint handles authentication but skips authorization entirely. It confirms the caller is a logged-in user but never checks whether that user has permission to access the requested resource. These are two completely different controls — and both are required.
Root Cause
One missing check:
const userId = req.query.userId;
// Never asks: does req.user.id === userId ?
The authenticated user’s identity is available on the request object — req.user from the JWT or session middleware. It’s simply never compared against the requested resource.
The Fix
Fix 1 — Always serve the authenticated user’s own data:
app.get('/api/user/profile', async (req, res) => {
// Use the authenticated user's ID from the token — not from the request
const userId = req.user.id;
const user = await db.query(
'SELECT id, name, email, phone, address FROM users WHERE id = ?',
[userId]
);
if (!user) {
return res.status(404).json({ error: 'User not found' });
}
return res.json(user);
});
The user ID comes from the verified JWT — not from user-controlled input. The parameter is removed entirely.
Fix 2 — If the ID must come from the request, validate ownership:
app.get('/api/user/profile', async (req, res) => {
const requestedId = parseInt(req.query.userId);
// Reject if the requested ID doesn't match the authenticated user
if (requestedId !== req.user.id) {
return res.status(403).json({ error: 'Access denied' });
}
const user = await db.query(
'SELECT id, name, email, phone, address FROM users WHERE id = ?',
[requestedId]
);
return res.json(user);
});
Fix 3 — For admin access to other profiles, add explicit role check:
if (requestedId !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Access denied' });
}
Key Takeaways
- Authentication and authorization are not the same thing — always implement both
- Never trust user-supplied IDs for resource ownership — derive the owner from the verified session or token
- IDOR is easy to miss in code review — look for any endpoint that accepts an ID parameter and fetches a resource without an ownership check
- Sequential integer IDs make IDOR trivial to exploit — consider UUIDs to reduce guessability, though this is not a substitute for proper authorization
- Test every resource endpoint with another user’s token — if you can access it, it’s vulnerable
Found this useful? More code reviews coming. Hit me up on X if you want to discuss.