Broken object-level authorization and IDOR¶
An authenticated user must still be authorized to read or change each requested object. An insecure direct object reference (IDOR), also called broken object-level authorization in API discussions, occurs when changing an identifier grants access outside that permission boundary. UUIDs and hidden form fields do not replace authorization. OWASP's IDOR guidance describes checking access for each object operation.
Trust boundary: an invoice ID in a route is a request for an object, not proof of ownership. An attacker may already know a valid ID. Possible impact includes cross-customer disclosure, unauthorized updates, or deletion, depending on the affected endpoint.
Unsafe example: authenticated but unscoped lookup¶
Python's sqlite3 placeholders prevent SQL injection in this example, but the query still ignores the user's permissions.
def read_invoice(db, invoice_id, actor):
return db.execute(
"SELECT id, total FROM invoices WHERE id = ?",
(invoice_id,),
).fetchone()
Safer example: enforce the application's ownership policy¶
def read_invoice(db, invoice_id, actor):
# actor comes from verified server-side authentication, never the request body.
row = db.execute(
"""SELECT id, total FROM invoices
WHERE id = ? AND tenant_id = ? AND owner_id = ?""",
(invoice_id, actor.tenant_id, actor.user_id),
).fetchone()
if row is None:
raise LookupError("Invoice not available")
return row
This example assumes only an invoice's owner may read it. A real shared-workspace policy may use explicit membership and role tables instead. Derive the tenant and user from authenticated server context; do not let a request select an unchecked active tenant. Map the exception to a consistent API response without disclosing whether an inaccessible invoice exists. The parameter binding follows the Python sqlite3 API.
For updates and deletes, include the same authorized scope in the mutation itself and check the affected row count. A separate check followed by an unscoped write can race with permission or ownership changes. Protect exports, nested resources, batch APIs, background jobs, and cached responses too.
Function-level authorization is a separate check. A correctly scoped invoice endpoint does not authorize an ordinary user to invoke an administrator's refund operation. Verify the permitted action as well as the object. Cookie-authenticated state changes may also require CSRF protection.
Regression test¶
Use a temporary database with two users in tenant A and one user in tenant B. Confirm the owner can read an invoice; another user in tenant A and every user in tenant B cannot. Test valid, missing, and inaccessible IDs with the same response contract. Repeat this matrix for write, delete, export, and administrative actions, asserting that denied requests leave data unchanged.
Related: JWT verification, mass assignment, and SQL injection.
References: CWE-639 — authorization bypass through a user-controlled key and CWE-862 — missing authorization.