Object-level authorization is the rule that decides whether this authenticated user may perform this action on this specific resource. It must run every time an API accepts an object identifier from the client—whether that identifier appears in the path, query string, header or request body.
OWASP places Broken Object Level Authorization, often shortened to BOLA, at the top of its 2023 API Security Top 10. The weakness is ordinary enough to hide in code review: the endpoint checks that a request is authenticated, loads an object by ID, and then returns or changes it without confirming that the caller is allowed to touch that object.
Authentication is only the first gate
Authentication answers who presented the credential. Object-level authorization answers what that identity may do with one record.
Consider a request such as:
GET /api/invoices/INV-1048
If the server finds invoice INV-1048 and returns it solely because the request carries a valid session, it has authenticated the caller but not authorized the object access. A customer who changes the identifier may reach someone else’s invoice.
The same problem appears in updates and deletes:
PATCH /projects/alpha/members/42DELETE /files/9f1...- a GraphQL mutation carrying
documentId; - a JSON body containing
account_id; - a background tool call that names a customer record.
The location and format of the identifier do not change the required decision.
Build the decision from user, action and object
A useful authorization decision has at least three inputs:
- the subject — the authenticated user, service or delegated actor;
- the action — read, update, delete, share, approve or another operation;
- the object — the particular invoice, project, file or customer record.
Context can add more: organization membership, delegated scope, record state, region, risk tier, or the relationship between two objects. The important part is that the loaded resource participates in the decision.
“User has the editor role” is not always enough. An editor may be allowed to change documents in one workspace but not every document in the database. “User belongs to tenant A” is also incomplete if the record can be shared, archived, locked or restricted to a team inside that tenant.
A compact policy might read: allow invoice.read when the invoice’s customer ID belongs to the caller’s account, or when the caller holds an audited support role with permission for that region. The exact rule is a product decision. The enforcement point should not invent it.
Scope the query before returning the object
One defensive pattern is to express ownership or membership inside the data lookup:
find invoice where id = requested_id and customer_id = current_customer_id
That reduces the chance that code loads a foreign record and forgets the next check. It also tends to return the same “not found” result for an object that does not exist and one the caller cannot see, avoiding unnecessary disclosure.
This pattern is not universal. Shared objects, administrative access and complex policies may require a separate policy engine or relationship check. But the principle survives: never let a client-supplied identifier become the whole authorization rule.
Repository and service interfaces can make the safe path easier. A method named getInvoiceForCustomer(invoiceId, customerId) communicates more than a generic getById. Code generation, framework guards and policy libraries can help, but tests must still prove the decision.
Random identifiers help less than they appear to
A long UUID is harder to guess than the number 1048. It reduces casual enumeration, which is useful. OWASP is explicit, however, that random identifiers are not a replacement for object-level checks.
Identifiers leak through logs, links, browser history, analytics, screenshots, email notifications and other API responses. They can also be obtained legitimately by a user who later loses access. Once an identifier is known, its unpredictability no longer protects the record.
Use non-sequential identifiers when they suit the product, but treat them as references, not capabilities. Possession of a URL or object ID should grant access only when the product intentionally defines a share link or capability token with its own scope, expiry and revocation rules.
Read and write permissions can diverge
A user may be allowed to view a project but not rename it; download a file but not share it; see an invoice but not issue a refund. Reusing one broad “can access” check across every method can create a write path that is more permissive than intended.
Review authorization by action:
| Action | Questions to answer |
|---|---|
| Read | May this actor see the object and every field returned? |
| Update | Which fields may change, and in which object states? |
| Delete | Is deletion allowed, and what dependent records are affected? |
| Share | May the actor grant access to someone else? |
| Approve | Is separation of duties required? |
| Export | Does the collection include objects outside the actor’s scope? |
Collection endpoints need the same care. Filtering a list after loading broad results can leak totals, identifiers or timing. Scope the query as early as practical and ensure pagination does not move a caller across an authorization boundary. Our note on API pagination consistency covers the ordering problem; authorization scope must remain stable across those pages too.
Field-level rules sit beside object-level rules
Passing an object-level check does not automatically authorize every field. A support agent may open a customer record while payment details, identity documents or internal risk notes remain restricted.
Mass-assignment bugs often meet BOLA at this boundary. The caller is allowed to update the object, but the server accepts fields such as owner_id, role, status or approved_by that the caller may not control.
Use explicit input schemas for each action. Read policies and write policies should identify sensitive fields. Avoid binding an entire request body directly onto the stored object.
This is also where purpose matters. Access allowed for support work should not silently authorize reuse for analytics or model training. Consent and purpose limits in data sharing examines that second boundary.
Test the negative space
Authorization tests should prove what the user cannot do, not only the successful path. Build fixtures with at least two users or tenants and test every method that accepts an object identifier.
A useful matrix includes:
- owner reads, updates and deletes the object as intended;
- another normal user cannot perform those actions;
- a user in the same organization but wrong team is rejected;
- a formerly authorized user is rejected after membership removal;
- an administrator receives only the actions the role grants;
- changing a parent ID cannot expose a child object;
- bulk and export endpoints cannot mix authorized and unauthorized objects;
- errors do not reveal whether a hidden object exists;
- cached responses do not survive a permission change.
OWASP recommends tests for object-level authorization and says not to deploy vulnerable changes. That sounds obvious, but it changes review practice: a feature is incomplete until its denial cases are automated.
Keep the policy near every entry point
A record can be reached through a REST route, GraphQL resolver, internal dashboard, import job, webhook handler or agent tool. Protecting only the public mobile endpoint leaves alternate paths behind.
Central policy logic helps the channels agree, but enforcement still belongs at each entry point. A service should pass the authenticated subject and requested action to the same decision, rather than relying on a controller that another caller may bypass.
The same principle applies to AI tools and delegated integrations. Our guide to MCP authorization separates connection identity, tool permission and user consent. When the tool names a customer or document, object-level authorization remains necessary inside the target service.
Log decisions without logging secrets
An authorization audit trail can record the actor ID, action, resource type, resource ID, policy version, result and correlation ID. It should not copy an entire sensitive object into the log.
Denied-request monitoring can reveal enumeration attempts, but alerts need context. A single stale link may be harmless; sequential probes across many object IDs deserve investigation. Do not turn the error response into a discovery oracle while making internal evidence too vague to investigate.
Questions for a design review
Is checking the tenant ID enough?
Only if the product’s complete policy is “every member of this tenant may perform this action on every object.” Team boundaries, roles, record state and sensitive fields often require more.
Do UUIDs prevent BOLA?
No. They reduce guessability. A known or leaked UUID still needs an authorization check.
Should an unauthorized object return 403 or 404?
Either can be correct. Many services use 404 when confirming existence would leak information. Keep the behavior consistent and log the internal reason.
Does an API gateway solve object-level authorization?
Usually not by itself. A gateway can authenticate and enforce route scopes, but the application often owns the relationship between the caller and the specific record.
Reporting & Sources
Checked 26 October 2026. Primary source: OWASP API Security Top 10 2023 — API1: Broken Object Level Authorization. This article applies the published risk and prevention guidance to product design and testing; it does not claim a penetration test of a particular service.





