Authorization

Enforce object-level authorization

Check the caller’s relationship to every requested object instead of accepting an object identifier as proof of access.

An object ID tells the server what to load. It does not tell the server who may load it. Broken object level authorization — BOLA, also known as IDOR — sits at the top of the OWASP API Security Top 10 because it keeps working: change one identifier in a URL, read someone else’s data.

Put ownership in the query

Resolve identity from trusted authentication state, then constrain the query by owner, tenant, membership, or policy:

SELECT id, total
FROM invoices
WHERE id = ? AND account_id = ?

The account_id value comes from the session, never from the request. If the row belongs to someone else, the query returns nothing and the API returns a plain 404.

Customer 42 can be signed in correctly and still request customer 43’s invoice. Random-looking IDs reduce guessing, but they never replace the ownership condition in the query. UUIDs leak through logs, referrer headers, and support tickets.

Test with two valid identities

Verification needs two real accounts, both authenticated, each trying to reach the other’s data:

# customer 42's token requesting customer 43's invoice
curl -i https://api.example.com/invoices/inv_9f3c2a \
  -H "Authorization: Bearer $TOKEN_CUSTOMER_42"
# HTTP/1.1 404 Not Found   <- the ownership check is working

Apply the same rule to read, update, delete, export, and nested routes. A team that protects GET /invoices/:id but forgets the DELETE handler has only decorated the problem.

Check indirect paths

Check indirect paths too. An attachment, comment, or export nested below an authorized invoice can still load a foreign child object unless every relationship stays constrained:

curl -i https://api.example.com/invoices/inv_own1/attachments/att_foreign \
  -H "Authorization: Bearer $TOKEN_CUSTOMER_42"
# must be 404: the attachment belongs to a different invoice

The classic mistake: the handler authorizes the parent invoice, then loads the attachment by its ID alone. The child query needs its own relationship condition, joining back to the authorized parent.

Create invoices for two valid accounts and exercise read, update, delete, and export. Save the response and database state proving every cross-account attempt failed without changing the target.

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →