The Endpoint That Trusts the Number

IDOR (Insecure Direct Object Reference) is the easiest authorization flaw to explain and among the most common to find: the application accesses an object by an ID the user supplies and never checks whether this user may touch that object. GET /order/1942 returns order 1942 to anyone who requests it, including a logged-in user who is not its owner. It is a broken-access-control instance — OWASP A01 — and it shows up constantly in real products because the happy path works fine for the owner and the check is simply absent.

Horizontal and Vertical Variants

There are two flavors. Horizontal IDOR: the attacker accesses a peer's object at the same privilege level — your account, your documents, your orders — by changing the numeric ID. Vertical IDOR / BFLA (Broken Function Level Authorization): the attacker accesses an administrative object or endpoint their role was never granted — a regular user calling the DELETE /users/{id} route the menu never shows. Both are the same root failure: the code checks authentication ("is anyone logged in?") but not authorization ("may THIS user access THIS resource?").

Why IDs Are Not the Problem

A common reflex is "make the IDs unguessable — UUIDs instead of incrementing integers". This is real hardening, not a fix. Obscurity fails on the first leak (an ID that appears in a link, a referer, an export, or a public listing is a known ID), and authorization cannot be achieved by making guessing harder. The correct control is invariant: every object access must pass an authorization check — the object is looked up scoped to the current user or the controller rejects unless the caller owns it:

def get_order(order_id: int, user):
    # SECURE: query scoped by owner — cross-user lookups return nothing
    return db.orders.find_one({"_id": order_id, "owner_id": user.id})

Scoped queries ("fetch my order") silently return None for foreign IDs and cannot be wrong; verifying ownership after a privilege fetch is equally acceptable as long as the check is unconditional.

APIs Make IDOR Worse

REST/GraphQL carve IDOR into its own edition: BOLA (Broken Object Level Authorization) heads the OWASP API Top 10. An API listing with ?ids=1,2,3 or a GraphQL mutation that accepts object IDs becomes a wholesale data dump if any non-owner ID is honored. Mass-assignment adds more: a client-supplied role field or a user_id in a body can let a normal call mutate another account. The defense is the same discipline plus strict DTOs: accept only the exact fields you need, and treat any ID in a request as a claim requiring an ownership check, never as a credential.

Finding and Fixing at Scale

Test systematically: for every object endpoint, swap the ID to another user's object and confirm a 403/404; then try a lower-privilege account against an admin object. Ensure the responses do not reveal existence (404 vs 403 — IDOR enumeration becomes an oracle if one leaks whether an object exists). Add authorization tests to CI that run as several roles against every route, and define the access rule in one middleware/guard rather than sprinkling ad-hoc checks in every handler — centralized gates cannot be forgotten inline. Object ownership is the single control that turns a quiet storage leak into a non-issue.