OWASP Top 10 Deep Dive · 2 of 8

Broken Access Control — The #1 Web Vulnerability

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?"

IDORAuthorizationRBACABACMulti-Tenant
← Back to Security
Quick Facts

What Broken Access Control Is

Basic Concepts

  • Authentication answers "who are you?" Access control answers "what are you allowed to do?" Both must be in place; getting one right does not protect the other.
  • Horizontal vs vertical: horizontal = accessing another user's data at the same privilege level. Vertical = escalating to admin / superuser.
  • The defining symptom: the API works correctly when you call it normally; it just doesn't check whether you specifically are allowed to call it for this specific resource.
  • Why it tops the list: there's no library that fixes it. Every endpoint, every resource, every action has to enforce its own check. Any one miss is a vulnerability.
The Family

How It Goes Wrong

IDOR — Insecure Direct Object Reference

The 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.

Missing Function-Level Authorization

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.

Forced Browsing

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."

Privilege Escalation via Mass Assignment

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.

Trusting Client-Supplied Role

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.

Cross-Tenant Data Leaks

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.

JWT Pitfalls

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.

Defenses

How to Get Access Control Right

1. Default Deny

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.

2. Centralize the Check

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.

3. RBAC vs ABAC
  • Role-Based (RBAC): users have roles; roles have permissions. Simple, clear, fits most apps.
  • Attribute-Based (ABAC): permissions are computed from attributes — user's department, resource's owner, time of day, environment. Right call when "admin in region X can edit Y but not Z."
  • Relationship-based (ReBAC): Google Zanzibar's model. Permission depends on graph relationships — "is user a member of the workspace that owns this document?" Tools: Auth0 FGA, SpiceDB, Oso Cloud.
4. Test Authorization

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.

5. Use Unguessable IDs Where Listing Isn't a Feature

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.

6. Audit Logs & Monitoring

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.

Continue

Other OWASP Top 10