A route-level check like "any authenticated user may GET /documents/:id" is not authorization. It only proves the caller can hit the endpoint.
Authorization asks: may this caller act on this document? Resolve the object, then evaluate a policy against the subject, action, and resource (and tenant if you have one). Skipping the object check is how IDOR happens — guessing or iterating IDs past a coarse gate.
Pattern: load the resource by ID from a trusted store, then allow or deny with an explicit decision. Never infer access from "they knew the URL."
Top comments (1)
The phrasing that matters here is 'resolve the object, then evaluate a policy' — too many codebases treat the route guard as the whole of authorization and never re-check the resource, which is exactly how IDOR sneaks in past a coarse gate. The tenant dimension is the one that usually bites in multi-tenant setups: even a correct object check can leak if the tenant isn't bound to the loaded resource.
One thing I'd add to the pattern: after resolving the object, make the decision explicit with a named function (e.g.
canAct(user, action, resource)) rather than an inline condition, so the object-level policy is testable in isolation and can't be skipped by a refactor that reorders the guards.