OWASP's #1 most common web vulnerability. Users access data or functions they shouldn't — by changing a URL ID, by hitting an admin endpoint that forgot to check, by escalating their role. These aren't crashes or clever exploits; they're missing checks. The code looks fine until you ask "who's allowed to call this?"
← Back to SecurityThe bug that ships every quarter. GET /api/orders/12345 returns the order if it exists — without checking that your account owns it. Change the number, see someone else's order.
Fix: every resource fetch must include an ownership predicate. WHERE id = :id AND tenant_id = :session.tenant_id. Don't trust the ID alone — bind it to the caller's context.
The admin UI hides the "Delete User" button for non-admins. The endpoint behind it doesn't check anything. Anyone who knows the URL — or guesses it — can delete users. Hiding in the UI is not access control; the server has to check on every request.
Endpoints exist that aren't linked from the UI — admin pages, debug routes, internal tools, old APIs left mounted. Attackers find them by wordlists and dictionaries. If they're reachable on the public internet, they need authentication and authorization, even if "no one knows about them."
The user profile form sends {name, email, isAdmin: true}. The framework happily binds isAdmin to the model and saves. Mass assignment lets the user set fields they were never meant to control.
Fix: use explicit DTOs / allowed-field lists. Spring's @JsonView, permit_params in Rails, Pydantic schemas, ASP.NET [Bind]. Never bind directly to your domain entity from request bodies.
The mobile app sends X-User-Role: admin. The server believes it. Anything sent by the client is attacker-controlled — roles, permissions, and identity must come from the server's session or token, not from the request body or headers.
Multi-tenant SaaS where tenant boundaries depend on developers remembering to filter by tenant_id on every query. One forgotten filter = data from another customer in the response. Mitigate with row-level security in the DB, query helpers that always inject the tenant predicate, and integration tests that try to fetch across tenants.
Trusting an unsigned token (alg: none), accepting any algorithm the token claims, not validating aud/iss/exp, storing roles in the token and never refreshing. Many "broken auth" stories are actually broken access control via JWT misuse.
The framework should refuse every request that doesn't have an explicit authorization check. Spring Security's permitAll() as the default is a footgun; require authenticated() + an explicit role/policy check. New routes are unreachable until someone declares who can use them.
Don't sprinkle if (user.role != ADMIN) return 403 through controllers. Use a policy layer — Spring Security's @PreAuthorize, ASP.NET policies, Casbin, OPA/Rego, Oso — that lives in one place and is reviewable.
Most test suites verify the happy path: "Alice can fetch her own order." Add the negative cases: "Alice cannot fetch Bob's order." "A free-tier user cannot call admin endpoints." Run them in CI on every PR. This is the single most effective protection against shipped IDORs.
Sequential IDs don't cause IDOR — missing checks do — but UUIDs in URLs make scanning attacks unproductive. Defense in depth, not a substitute for proper authorization.
Log every authorization denial with user, resource, and reason. Sudden spikes in 403s often mean someone's probing. Alert on patterns — a single user denied across 100 different IDs in a minute is suspicious.