Stop 05 · Vulnerabilities & OWASP Top 10Topics · 9/11

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.

The blob stands behind a wooden cloakroom counter with tagged handbags on hooks as a hand pushes a token across.

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

User 7
API
Database

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

ts
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

~30 min

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

  1. 1Test a practice app for an Insecure Direct Object Reference (IDOR) by manipulating a resource ID.
  2. 2Learn why this is consistently the most reported OWASP category in real-world audits.
  3. 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.

Download
InsideEdge