Broken Object Level Authorization (BOLA)
Broken Object Level Authorization is a flaw in which a service lets an authenticated caller read or change an object by naming it, without checking that this caller may touch that particular object. It is also called insecure direct object reference (IDOR). OWASP lists it first in its API Security Top 10 (2023 edition).
What goes wrong
Many APIs identify records by an id in the path or body, such as GET /orders/B-1. Authentication establishes who is calling. Authorization has to answer a separate question for each request: may this caller access this object? A handler that looks the record up by id and returns it never asks the second question: the caller may be authenticated, but finding a record by its id does not show that this caller may access it. Any caller who can guess, enumerate, or be handed another user's id gets that user's data. Changing the id is the entire attack; no special payload is needed.
The check is called object level because it depends on the relationship between the caller and the specific record, typically ownership or membership in a team. It differs from function-level authorization, which asks whether the caller may use the operation at all, for example whether a read-only user may call refund. A service can pass the function-level check and still fail at the object level, because both users are allowed to call get_order.
A small trace
The program below creates a hypothetical in-memory database of three orders and two handlers.
import sqlite3
db = sqlite3.connect(":memory:")
db.executescript("""create table orders(id text primary key, owner text, address text);
insert into orders values('A-1','ana','1 Elm St'),('A-2','ana','2 Elm St'),('B-1','ben','9 Oak Ave');""")
def get_order_broken(user, oid):
return db.execute("select id, owner, address from orders where id=?", (oid,)).fetchone()
def get_order_fixed(user, oid):
return db.execute("select id, owner, address from orders where id=? and owner=?", (oid, user)).fetchone()
When ana asks for B-1, the broken handler returns ('B-1', 'ben', '9 Oak Ave') and the fixed handler returns None. The only difference is that ownership is part of the query condition, so a record the caller may not see is indistinguishable from one that does not exist. Returning the same answer for both also avoids confirming which ids are valid.
Notice that the broken handler uses a parameterized query and is not vulnerable to SQL injection. Object-level authorization and injection are independent defects.
Why unpredictable ids are not a fix
Using random identifiers makes enumeration harder, and OWASP recommends them as a supplement. They do not replace the check, because ids leak through logs, shared links, emails, and, in agent systems, through model context. A tool that accepts any id the model emits is exposed in exactly this way: the model's output is a client-supplied identifier. The remedy is to authorize against the authenticated user on every function that uses such an id to reach a record, which is the same principle as avoiding a confused deputy.
Testing
A useful test treats the authorization rule as data. For every object and every user who does not own it, call the handler and expect a refusal.
def cross_user_test(handler):
leaks = []
users = [r[0] for r in db.execute("select distinct owner from orders")]
for oid, owner in db.execute("select id, owner from orders").fetchall():
for u in users:
if u != owner and handler(u, oid) is not None:
leaks.append((u, oid))
return leaks
Running it against the sample data, the broken handler reports [('ben', 'A-1'), ('ben', 'A-2'), ('ana', 'B-1')] and the fixed one reports []. The test is cheap to generate for every tool that takes an identifier, and can run in continuous integration so that a new endpoint without the check fails the build. Include write operations and nested objects (an order's line items, an attachment of an order), since the check is frequently present on the parent and missing on the child.
References
Discover more from Insightful Data Lab
Subscribe to get the latest posts sent to your email.
