Module 14 · Operator
API Authorization & BOLA
Broken object-level authorization as its own lesson. The web module mentions IDOR in passing. Here you learn the idea, how a professional tests it only with authorized lab accounts, how to recognize it, and how to fix it. No request templates and no exploit payloads.
Authorized testing only. Practice on systems you own, isolated labs, or targets with written permission. Unauthorized access is illegal.
Outcomes
- Explain object-level authorization in one sentence a product owner understands
- Design a two-account lab check that stays inside written scope
- Recognize the failure modes without walking identifiers
- Recommend server-side fixes and regression tests
Lessons
What broken object-level authorization is
BOLA is OWASP API Security's leading risk: the API checks that someone is logged in, then lets that someone name any object. The bug is missing authorization, not a broken password.
Learning objectives
- Separate authentication from authorization
- Define an object, an owner, and an action
- Contrast horizontal access between peers with vertical access to admin functions
- Explain why an unguessable identifier is not a control
Deep teach-through
The idea
Authentication answers who is calling. Authorization answers what that caller may do to this specific object. Broken object-level authorization (BOLA), often discussed alongside IDOR, is when the API accepts an identifier — in the path, the query, or the body — and returns or changes the record without checking that this caller owns it or is allowed to touch it.
Picture a notes API. Account A created note 100. Account B created note 200. Both are ordinary users. If A's session can read note 200, the API authenticated A and then skipped the ownership check. That is horizontal BOLA. If A can call an admin-only action such as listing every tenant's notes, that is a vertical authorization failure. Both matter. They are different findings because the fix and the impact differ.
The web curriculum already uses IDOR as one cell in a broader test matrix. This module is the dedicated treatment for APIs, where identifiers are the product. GraphQL nodes, REST resources, and batch endpoints all have the same question: did the server bind this object to this caller?
Why it keeps shipping
Frameworks often give you authentication middleware and leave authorization to each handler. A team copies a 'get by id' function, checks the session cookie, and ships. The happy path works. The missing line is the one that compares the object's owner to the session.
Clients sometimes send an owner field, a tenant id, or a role, and the server trusts it. Hidden form fields and JSON bodies are still caller-controlled. The trustworthy identity is the one the server derived from the session or the validated token, not a field sitting next to the data.
Unguessable identifiers (UUIDs, random strings) reduce casual browsing. They do not authorize the caller. If any other user, log, or feature reveals the identifier, the record is exposed whenever the check is missing. Security by obscurity is not the control you recommend.
Impact, stated for a report
Impact depends on the object. A profile name is not a medical record. Say what the object contains, who else could reach it, and what action succeeded: read, change, or delete. Business impact is confidentiality of that data, integrity if it was modified, and availability if it was removed.
A single authorized proof is enough when the rules of engagement say so. The finding is the missing check, demonstrated with accounts the client provided. It is not a tour through production data. If you can already see another customer's real content, stop, preserve the minimum evidence you agreed to keep, and notify the emergency contact.
Map the issue to OWASP API Security Top 10 API1 (Broken Object Level Authorization) and, when function-level gaps are separate, API5. Do not inflate a missing check into a different vulnerability class.
Key concepts
- BOLA
- The API fails to check that this caller may access this specific object.
- Authentication
- Proof of who the caller is. Necessary, not sufficient.
- Authorization
- The server's decision that this caller may perform this action on this object.
- Horizontal access
- A user reaches a peer's object at the same privilege level.
- Object identifier
- The value that selects a record. It is an address, not a permission.
Common mistakes
- Calling every authorization bug 'IDOR' and skipping function-level issues
- Treating a UUID as the remediation
- Proving the bug by opening as many real customer records as possible
- Trusting a tenant id supplied by the client
Defender view
- Authorize in one shared server-side helper so new endpoints cannot forget it.
- Log denied cross-object attempts with the caller and the object type, not the full record.
- Regression tests with two fixtures belong in CI, next to the endpoint.
Operator checklist
- I can point at the object type and the action, not just 'the API'
- I am using accounts and an environment named in the rules of engagement
- I will stop once the missing check is shown
- The write-up states the fix, not a shopping list of other people's records
Practice drills
- Write the one-sentence definition of BOLA you would put in an executive summary
- Sketch a 2x2 matrix: accounts A and B, objects A and B, and which cells must be denied
- Explain to a teammate why a random identifier is not authorization
Tools for this lesson
Next: Next: how to test that matrix only in an authorized lab, and how to recognize the failure.
Authorized testing, recognition, and the fix
How a professional checks object-level authorization with two in-scope accounts, how the failure shows up, and what to recommend. This is judgment and recognition, not a payload.
Learning objectives
- List the scope fields required before any cross-account check
- Describe a two-account comparison without a request template
- Recognize a missing ownership check from the behavior
- Recommend server-side authorization and a regression test
Deep teach-through
Scope before any comparison
Written permission should name the API host, the environment (a non-production tenant whenever one exists), the test accounts, and whether reads are enough or changes are allowed. Production customer data is out of scope unless the contract says otherwise in writing. If the only environment is production, stop and ask for a lab tenant or an explicit decision from the owner.
You need two identities the client gave you, and objects those identities are supposed to own. Creating those objects yourself inside the test tenant is cleaner than borrowing real ones. Agree in advance what evidence looks like: a status code, whether the body differed, and a redacted screenshot. Agree what you will not keep.
Out of scope by default: other customers, other tenants, identifiers you found outside the test data, bulk enumeration, and any technique that changes or deletes data you were only allowed to read. A curious identifier is not an invitation.
The lab comparison, as a method not a recipe
Sign in as account A through the normal product flow. Note one object that belongs to A. Sign in as account B and note a different object that belongs to B. The question you are answering is whether A's session is refused when it asks for B's object, and whether B is refused for A's object.
Use the application's own interface first. A proxy such as Burp or ZAP is there so you can see whether the server, not the hidden button, made the decision. You are looking at the outcome: allowed, denied, or a body that contains the other account's data. You are not building a list of identifiers and you are not publishing a request to copy.
If the response for the other account's object contains that account's data, you have a candidate finding. Capture the minimum proof the rules allow, then stop. Do not continue across further records to 'see how bad it is.' Severity comes from the data class and who else could do the same, which you can explain without a tour.
Function-level gaps are the sibling test: can a normal test user open an admin-only action the client said should be denied? That is a separate note even when it sits on the same API. Do not fold it into BOLA just because both are authorization.
How to recognize it, and how to fix it
Recognition signs: any authenticated caller can name a record and receive it; the server accepts an owner or tenant field from the client; a denied user interface still succeeds when the call is repeated another way; creating an object as A and reading it as B works; admin fields appear for a standard test user. A 403 or an empty 404 on the other account's object is what you hope to see. A 200 with the other account's content is the failure.
Inconsistent status codes are a product decision, not automatically a vulnerability. Document what happened. The vulnerability is the missing check, not the choice between 403 and 404, unless that choice leaks the existence of objects the caller must not know about and the client cares about that leak.
The fix is server-side: every object access and mutation resolves the caller from the validated session or token, loads the object, and allows the action only if policy says so. Ignore client-supplied owner fields. Put the check in shared code. Add a regression test with two fixtures so the next endpoint cannot ship the hole again. Log the denial. Rate limits and random identifiers are useful, and they are not the fix.
Key concepts
- Two-account check
- Compare what account A may do with A's object versus B's object, using only accounts in scope.
- Minimum proof
- Enough evidence to show the missing check, and no more records than the rules allow.
- Server-side policy
- The authorization decision lives in the API, derived from the caller the server authenticated.
- Regression test
- An automated check that account A is denied for account B's fixture object.
Common mistakes
- Starting on production because the staging API was empty
- Walking every identifier after the first cross-account read succeeded
- Putting a reusable request in the report
- Recommending 'use UUIDs' as the only remediation
- Testing accounts you registered on a live multi-tenant site without a bounty or a contract
Defender view
- Threat-model each new resource: who owns it, who may read it, who may change it.
- Fail closed when the ownership lookup fails.
- Review authorization in code review the same way you review authentication.
Operator checklist
- Environment, API, and both accounts are named in the RoE
- I created or was given the test objects; I did not harvest them
- I stopped at the first clear cross-account success
- The finding lists object type, action, impact, and the server-side fix
- Evidence is redacted and stored with the engagement
Practice drills
- Draft the rules-of-engagement paragraph you would require before a BOLA check
- Write the recognition signs in your own words, with no sample requests
- Write a five-line remediation a developer can implement without guessing
Tools for this lesson
Next: Secret handling in pipelines is a different trust boundary. Take the CI/CD module next.