CWE-639CWE-285OWASP A01:2021 Broken Access Control

Broken access control & IDOR

The application checks who you are but not whether you're allowed to touch a specific record or function, so changing an ID or calling an endpoint directly exposes other users' data or admin actions.

Also known asInsecure direct object reference · Broken object level authorization (BOLA) · Missing function level access control
Typical severityMedium to Critical
ClassificationCWE-639, CWE-285
Last updatedOct 14, 2026

What it is#

Access control decides what an authenticated user may do. It breaks when the server trusts the client to only ask for things it's allowed to see.

  • IDOR / BOLA: an endpoint like GET /api/invoices/1043 returns invoice 1043 to any logged-in user, so changing the number returns someone else's invoice.
  • Missing function-level checks: admin endpoints are hidden in the UI but not protected on the server.
  • Privilege escalation through writable fields: a user can set role, tenant_id or is_admin in a profile update (mass assignment).
  • Multi-tenant leaks: queries filter by record ID but not by the caller's organization.

Why it matters#

  • Bulk exposure of other customers' personal or financial data, often by simply counting through IDs.
  • Unauthorized changes: editing or deleting other users' records, approving your own requests.
  • Vertical escalation to admin functions, and in multi-tenant SaaS, crossing from one customer to another.
  • These bugs are easy to exploit, hard to see in logs because requests look normal, and are consistently the top category in the OWASP Top 10.
Typical severity

Medium when exposure is limited or non-sensitive. High to critical when it exposes regulated data at scale, crosses tenants, or grants administrative actions.

Examples#

javascript
// Vulnerable: authenticated, but no ownership check
app.get('/api/invoices/:id', requireLogin, async (req, res) => {
  const invoice = await db.invoice.findUnique({ where: { id: req.params.id } });
  res.json(invoice);
});
python
# Vulnerable: mass assignment lets a user change their own role
user.update(**request.json)   # {"display_name": "Sam", "role": "admin"}

How to fix it#

1. Enforce authorization on the server for every request, scoped to the caller:

javascript
// Fixed: scope the lookup to the caller's account
app.get('/api/invoices/:id', requireLogin, async (req, res) => {
  const invoice = await db.invoice.findFirst({
    where: { id: req.params.id, accountId: req.user.accountId },
  });
  if (!invoice) return res.sendStatus(404);
  res.json(invoice);
});

2. Centralize policy. Put authorization in one place (middleware, a policy library such as CASL, Pundit, OPA or Cedar) instead of ad hoc checks in each handler. Deny by default.

python
# Fixed: explicit allow-list of writable fields
ALLOWED = {"display_name", "timezone"}
user.update(**{k: v for k, v in request.json.items() if k in ALLOWED})

3. Protect functions, not just pages. Every admin or privileged API route must check the role or permission server-side, whether or not the UI shows the button.

4. Make IDs harder to guess. Random UUIDs reduce enumeration but are not a fix on their own; the ownership check is.

5. Log and alert on authorization failures and unusual access patterns, such as one user reading many different accounts' records.

How to test for it#

  • Use at least two accounts per role (and two tenants if multi-tenant). Capture requests as user A and replay them with user B's session.
  • Change every identifier you can see: path segments, query parameters, JSON body fields, headers, and IDs embedded in encoded or signed-looking values.
  • Call admin and internal endpoints directly as a low-privilege user.
  • Try adding privileged fields (role, isAdmin, tenantId) to update requests.
  • Automate the matrix (role × endpoint × object owner) in integration tests so regressions are caught before release.

References#