AuthorizationIDORBOLAaccess controlobject-levelenumeration

Broken Access Control (IDOR / BOLA)

GET /invoices/100 works; GET /invoices/101 also works and it is not yours. Authentication succeeded, authorization was never asked — the most common serious vulnerability in web applications and APIs.

▶ Run the labFollow the failure

Frame the problem

Security starts with a concrete asset, attacker capability and trust crossing.

Asset
Every record addressable by an identifier the client supplies.
Attacker & capability
Any authenticated user with a browser's developer tools.
Trust boundary
The record-level boundary between requester and resource owner.
AssetThreatAttack SurfaceTrust BoundaryVulnerabilityExploit PathImpactMitigationDefense in DepthResidual Risk

Why it is so common

The bug is an absence, not a mistake. A handler loads a record by id and returns it; nothing in it is wrong, and nothing in it checks ownership. Tests pass because tests use the owner's account. Scanners cannot see it because the response is a valid 200. Frameworks cannot prevent it because they do not know which user owns invoice 101.

It appears wherever a client-supplied identifier selects a record: path parameters, query strings, JSON bodies, file keys, websocket messages, GraphQL arguments, and the second page of a paginated list.

Loads by id, returns
1app.get('/invoices/:id', requireLogin, async (req, res) => {
2 res.json(await invoices.byId(req.params.id))
3})
Loads within the principal's scope
1app.get('/invoices/:id', requireLogin, async (req, res) => {
2 // The repository method REQUIRES a principal and scopes the query:
3 // SELECT ... WHERE id = $1 AND tenant_id = $2
4 const inv = await invoices.forPrincipal(req.principal).byId(req.params.id)
5 if (!inv) return res.sendStatus(404) // absent and forbidden look identical
6 res.json(inv)
7})

The fix that survives is structural: a data-access API that cannot be called without a principal, so the tenant/owner predicate is part of the query rather than an optional check after it.

Variants worth knowing

Write-side: PUT /invoices/101 with a check on read but not on update. Indirect: the id of a parent is checked but the child (/invoices/100/lines/5019) belongs to another invoice. Function-level: /admin/export guarded only by not being linked. Identifier in the body: {"userId": 124} accepted rather than taken from the session. Batch and search: a filter parameter that lets a query cross tenants.

Unguessable ids (UUIDs) reduce casual enumeration and are worth using, but they are depth, not the control: ids leak through references, logs, exports and other users' pages.

Key points

  • Authentication succeeded, authorization was never asked.
  • The response is a valid 200, so scanners and tests miss it.
  • Fix structurally: data access that requires a principal.
  • Check every place a client supplies an id, including bodies, children and filters.
  • The single most valuable security test: user A cannot read user B's record.

Boundary control exercise

This lesson uses the shared boundary-control exercise.

Boundary control check
Untrusted input / identity
Trust boundary
Privileged asset
Prevention may fail silently.

Follow the attack

Safe conceptual simulation: capability → missing control → crossed boundary → asset impact.

  1. 1
    Attacker → own account, observe ids.
  2. 2
    Substitute an id in path, body, or filter.
  3. 3
    Iterate the id space; export.
Blast radius
  • Entire record type exposed to any customer; often the whole customer base.

Defend, detect, recover

One prevention is a single point of security failure. Layer it and make failure observable.

Prevent
  • • Principal-scoped repositories.
  • • Take the user id from the session, never the request.
  • • Per-resource A-cannot-read-B tests.
  • • Opaque ids as depth.
Detect
  • • One principal, many distinct ids, short window.
  • • Requester ≠ owner without sharing relation.
Respond & recover
  • • Access logs → which records were read; fix all sibling endpoints; notify.
Residual risk
  • • New endpoints; sharing rules; admin tools.

Misconceptions

Claim
“UUIDs fix IDOR.”
Reality
They make guessing harder. Ids still leak, and the missing check is still missing.