Broken Access Control
Part of the Cybersecurity Roadmap
In short
Logged in is not the same as allowed. When a server forgets to check whether this record belongs to this user, changing one number in a URL can expose someone else's data.
Failures in enforcing what an authenticated user is actually allowed to do — accessing another user's data by changing an ID in a URL is a classic, extremely common example.

Think of it like this
The cloakroom that never checks tokens
At a wedding cloakroom, you hand over your bag and get token 41. If the attendant hands back any bag to anyone who says a number, people walk off with others' bags. A good attendant matches your token to the tag every time.
Watch it happen
Ownership checked on every request
GET /orders/42
1/6User 7 opens their own order. Their session proves who they are.
The big ideas
What you need to know
Check on the server
Hiding buttons in the UI isn't access control. Anyone can call your API directly with curl or Postman.
Deny by default
Every route should require an explicit rule to allow access. A forgotten rule should mean "no", not "yes".
Ownership and roles
Check both: does this user's role allow the action, and does this record belong to them or their team?
Log the denials
Many 403s or 404s from one account can mean someone is probing. Log them and alert on spikes.
See it in code
Walk through the code
1function canEdit(user, post) {2 return user.role === "admin" || post.authorId === user.id;3}4 5app.patch("/posts/:id", requireLogin, async (req, res) => {6 const post = await db.post.findUnique({ where: { id: req.params.id } });7 if (!post || !canEdit(req.user, post)) {8 return res.status(404).json({ error: "Not found" });9 }10 const { title, body } = req.body; // never copy role or authorId from input11 const updated = await db.post.update({ where: { id: post.id }, data: { title, body } });12 res.json(updated);13});1/3The rule: admins can edit anything, everyone else only their own posts.
One small policy function per resource keeps the rule in a single place, so every route asks the same question the same way.
Save yourself the pain
Common mistakes
Oops
Using random-looking IDs like UUIDs and calling the records private.
Do this instead
Hard-to-guess IDs help a little, but IDs leak. Always check ownership on the server.
Oops
Checking permissions in some routes and "adding the rest later".
Do this instead
Use middleware or a policy layer that applies to all routes, with deny as the default.
Your mission
Two-user test on your own app
Quick check
The admin page is hidden from normal users in the menu, but GET /api/admin/users has no role check. What's true?
Go deeper
- 1Test a practice app for an Insecure Direct Object Reference (IDOR) by manipulating a resource ID.
- 2Learn why this is consistently the most reported OWASP category in real-world audits.
- 3Understand the fix: server-side authorization checks on every request, never trusting client-side hiding alone.
More topics at this stop
Stuck on this topic? Ask an Insider
Get 1:1 guidance from people who've walked this exact path — on the InsideEdge app.