Broken Object Level Authorization (BOLA / IDOR)
ID |
broken_object_level_authorization |
Severity |
high |
Remediation Complexity |
medium |
Remediation Risk |
medium |
Remediation Effort |
medium |
Family |
API1:2023 - Broken Object Level Authorization |
CWE |
CWE-639, CWE-284 |
Resource |
authorization |
Language |
any (model-level) + Java / C# (AST Phase C) + Python / JS / Go / PHP (regex Phase B) |
Description
Reports API endpoints that access objects by an ID-shaped path or query parameter (/orders/{id}, /users/:id, /documents?id=…) and require authentication, but show no model-level evidence that the handler verifies the caller’s ownership of the referenced object.
This is the classic Insecure Direct Object Reference (IDOR) pattern and the OWASP API1:2023 — Broken Object Level Authorization — entry. Per-language source-walking (Phase B) can lift the verdict to HIGH when no explicit ownership check is found in the handler body; AST-based confirmation (Phase C) extends the check across one-hop helper calls and ORM-side row scoping in Java and C#.
Rationale
Authentication confirms who the caller is. Object-level authorization is the orthogonal question — which objects is this caller allowed to see or change?
An endpoint that returns the order whose id is in the URL — without verifying the order belongs to the caller — lets any authenticated user enumerate every order in the system simply by incrementing the id. BOLA is the most-reported finding in public API breach catalogs (OWASP API1 since 2019 and unchanged at the top in 2023). Real-world consequences include cross-tenant data leaks in B2B SaaS, mass enumeration of medical records, and credential / token theft via account-recovery flows that fetch by user id.
Authorization is not "did I write a check?" but "does the check actually scope by user identity?" — a frequent failure mode is a guard that confirms the caller is logged in (already enforced by auth) without comparing the resource owner to the caller, leaving BOLA wide open.
Remediation
The fix is to bind every object-by-id access to the caller’s identity at the data layer or via an explicit guard. In order of robustness:
-
Repository-scoped queries — the strongest. Replace
findById(id)withfindByIdAndOwner(id, currentUser)(Spring Data),Model.objects.filter(user=request.user).get(pk=id)(Django),findOne({_id: id, userId: req.user.id})(Mongoose),Where("user_id = ?", userID).First(&order, id)(GORM), orOrder::where('user_id', auth()→id())→findOrFail($id)(Eloquent). The caller cannot escape the filter. -
Policy / authorization framework —
@PreAuthorize("@bookService.canRead(#id, principal)"), ASP.NETIAuthorizationService.AuthorizeAsync(user, resource, policy), Laravel$this→authorize('view', $order), DRFcheck_object_permissions(request, obj). -
Explicit guard — load the object, compare
obj.userId == currentUser.id, return 403 / 404 on mismatch. Acceptable but easy to skip on a refactor; prefer the patterns above.
Avoid relying on "the user only sees their own ids in the UI" — assume any authenticated caller will iterate the id space.
Configuration
The detector recognises a configurable set of id-shaped parameter patterns (id, *Id, *_id, uuid) — patterns can be extended via the idParameterPatterns property. The model-level pass is the cheap floor; per-language Phase B / Phase C source walking is automatic when the handler file is parseable.
Two further properties tune what counts as an ownership check:
-
mediatorMethods— per-language names of your own authorization helpers (assertCanAccess,check_object_permissions). A call to one inside the handler counts as an ownership check. Do not list role checks or authentication-only helpers here: they answer a different question and would silence the finding on every guarded endpoint. -
privilegedRoles— role names whose holders are meant to reach objects that are not their own, so that an endpoint gated on one of them is not reported. The shipped list covers the administrator / root family in English (admin,administrator,superadmin,superuser,root,sysadmin) and replaces, rather than extends, the default when you set it — an application whose admin role isgestororbeheerderlists it here to avoid a false positive on every admin endpoint. Names are compared whole and case-insensitively with Spring’sROLE_/SCOPE_prefix stripped, soTENANT_ADMINis not treated as privileged (a tenant admin reaching another tenant’s object by id is the canonical multi-tenant BOLA), and a list of alternatives counts only when every name in it is privileged (hasAnyRole('ADMIN','USER')lets an ordinary user in). Scope-ambiguous roles —manager,support,staff,moderator,operator— are deliberately not shipped as privileged, because "a support agent can read any customer record by id" is a finding most teams want to see; add them if your application intends otherwise.
Unless your project uses unusual id-parameter naming, non-English role names, or its own authorization helpers, you typically do not need to configure this detector.
References
-
OWASP API Security Top 10 (2023) - API1:2023 - Broken Object Level Authorization.
-
CWE-639 : Authorization Bypass Through User-Controlled Key.
-
CWE-284 : Improper Access Control.
-
OWASP Cheat Sheets Series: Authorization Cheat Sheet.
-
OWASP Cheat Sheets Series: Insecure Direct Object Reference Prevention Cheat Sheet.