Definition
Broken access control happens when an application correctly figures out who a user is (authentication works fine) but fails to correctly enforce what that specific user is allowed to do or see (authorization fails). The login worked. The problem is everything that was supposed to happen after it.
The Trust Boundary That Breaks
Most developers build a clear check for “is this request coming from someone who’s logged in.” Far fewer build an equally consistent check for “is this specific logged-in user allowed to touch this specific record.” Those are two different questions, and a codebase can answer the first one perfectly on every single endpoint while quietly skipping the second on just a handful of them.
The trust boundary that actually needs to hold is per-record, not per-session: a valid session proves identity, nothing more. The moment a developer treats “has a valid session” as equivalent to “is authorized for this exact resource,” that boundary is gone, and the only thing standing between an attacker and someone else’s data is whatever value happens to sit in a URL or request body.
Where It Actually Shows Up
- Direct object references: endpoints like
/api/invoices/1042or/documents/edit?id=1042, where the ID itself is the only thing standing between a user and someone else’s record. - Horizontal privilege escalation: a regular user reaching another regular user’s data, without any elevation in role, just a different record.
- Vertical privilege escalation: a regular user reaching admin-only functionality, often because an admin endpoint checks for a valid session but never checks the session’s role.
- Client-side-only enforcement: a “hidden” admin button or disabled form field that’s absent from the rendered page but whose backend endpoint never independently verifies the caller’s privilege level, so the check can simply be skipped by calling the API directly.
Why It Keeps Happening
Authorization logic is easy to write once and easy to forget to repeat. A developer adds a proper ownership check on the first version of an endpoint, then a second, similar endpoint gets added later (often by a different person, under deadline pressure) that copies the general shape of the first but omits the ownership check, because it “obviously” needs the same protection and nobody circled back to confirm it actually has it. Authorization, unlike authentication, rarely lives in one central, reusable place; it has to be re-implemented correctly on every single endpoint that touches user-specific data, and consistency is genuinely hard to maintain at scale.
How to Find It
- Enumerate every endpoint that accepts an identifier (an ID, a UUID, a filename) referencing a specific record.
- Using a valid, authorized test account, note a record it legitimately owns, then swap in the identifier for a different record owned by a separate, unrelated test account.
- Confirm the response is properly rejected. A generic error page is not proof of rejection; check that no unauthorized data was actually returned in the response body, since some endpoints fail “gracefully” on the surface while still leaking data underneath.
- Repeat the same test for state-changing requests (edit, delete), not just reads: an endpoint that correctly blocks viewing another user’s record can still fail to block editing or deleting it.
- Test vertical escalation separately: using a low-privilege account, call admin-only endpoints directly rather than relying on the UI to hide them.
Impact by Scenario
| Scenario | Realistic impact |
|---|---|
| IDOR on a low-sensitivity, read-only resource | Limited exposure of non-sensitive data; still a real finding, but bounded |
| IDOR on financial, health, or personal records, read access only | Serious privacy and regulatory exposure, scaled by how many records are sequentially or predictably enumerable |
| IDOR allowing edits or deletes on another user’s records | Data integrity failure on top of exposure; an attacker can alter or destroy what they were never authorized to touch |
| Vertical escalation reaching genuine administrative functionality | Full compromise of the application’s own trust model; the attacker now operates with the application’s highest privilege level |
Why a Business Should Care
Broken access control is consistently one of the most common findings across real-world assessments, and it is often the cheapest class of finding for an attacker to exploit: no exotic payload is required, just a predictable ID and a willingness to change it. For a client, the useful framing is that this class scales with data volume. A single missed ownership check on a customer-record endpoint is not a one-off leak; it is a template an attacker can run against every sequential ID in the system, which is exactly why a single broken endpoint of this kind has produced some of the largest mass data exposures on record. It is also a class where “our authentication is strong” gives a client zero reassurance, since the entire failure happens after authentication succeeds.
A Worked Example
(Generalized from real assessment patterns. Company and identifiers below are invented; no real client, system, or data is referenced.)
Harrow Ridge Billing runs a customer portal where authenticated users view their own invoices at GET /api/invoices/{id}. Logging in as a legitimate test account and viewing that account’s own invoice returns the expected record at, for example, ID 4471.
Incrementing the ID by one, to 4472, while still authenticated as the same test account, returns a complete invoice belonging to a different, unrelated customer: full billing address, itemized charges, and the last four digits of a payment method. The endpoint checks only that a valid session exists; it never checks whether the session’s owner actually owns invoice 4472.
Testing continues by sampling a handful of further sequential IDs, confirming the pattern holds consistently rather than being a one-off anomaly, and confirming the same flaw exists on the equivalent PDF-download endpoint. Testing stops there: enough evidence exists to prove the entire invoice range is enumerable, without actually harvesting real customer data beyond what was needed to demonstrate the finding.
sequenceDiagram
participant Attacker as Authenticated User (Account A)
participant Portal as Customer Portal API (/api/invoices/)
participant DB as Billing Database
Note over Attacker,Portal: Step 1: Legitimate query for owned resource
Attacker->>Portal: GET /api/invoices/4471 (Session: User A)
Portal->>DB: Query invoice 4471
DB-->>Portal: Return invoice 4471 record
Portal-->>Attacker: HTTP 200 OK (User A's Invoice)
Note over Attacker,Portal: Step 2: Modifying object reference to Target B
Attacker->>Portal: GET /api/invoices/4472 (Session: User A)
Portal->>Portal: Verify Session Valid? (YES)
Note right of Portal: Flaw: Server fails to verify if User A owns 4472
Portal->>DB: Query invoice 4472
DB-->>Portal: Return invoice 4472 record (Belongs to User B)
Portal-->>Attacker: HTTP 200 OK (CRITICAL: Full PII & Financial Data Leaked)
Severity Calibration
This instance rates Critical: unauthenticated in the sense that matters most (any valid low-privilege account, not just a compromised admin account, can reach it), trivially exploitable by anyone who can count, and demonstrated against real financial and personal data across a range of sequential IDs rather than a single isolated record. A version of this same bug on a resource containing nothing sensitive, or requiring a non-sequential, non-guessable identifier that isn’t practically enumerable, would rate meaningfully lower; the identifier’s predictability and the data’s sensitivity are doing the real work in this rating, not the vulnerability class by itself.
Remediation
The real fix is enforcing an ownership or permission check on every request that references a specific record, implemented server-side, on every endpoint, every time, ideally through a shared, centrally-tested authorization layer rather than hand-rolled per endpoint. Using non-sequential, non-guessable identifiers (a UUID rather than an incrementing integer) is a reasonable defense-in-depth layer on top of that, but it is not a substitute for it: a UUID that leaks once through logs, referrer headers, or a different flaw is just as exploitable as a sequential ID if the ownership check itself is still missing.
The common bad fix is relying on obscurity: assuming that because an ID is a long random-looking string, or because a link is not directly advertised anywhere in the UI, nobody will find it. Anything reachable by a request from an authenticated session is reachable by an attacker with a valid account of their own, and obscurity alone has never been a real access control.
Related Classes
- SQL Injection: a structurally different root cause, but access control failures are frequently what a chained SQL injection exploit ultimately unlocks once an attacker is inside a system.
- Insecure Direct Object Reference (IDOR): the single most common real-world realization of this exact vulnerability class, covered in its own dedicated depth.
- Cross-Site Request Forgery (CSRF): a different mechanism for reaching the same outcome, tricking a user’s own browser into an unauthorized action rather than directly manipulating an object reference.
- Path Traversal: another access-control failure, at the filesystem layer instead of the object-reference layer.
- Open Redirect: a lower-severity, but structurally related, access-control gap around trusting an unvalidated destination.
- Cross-Tenant Isolation Failure: the same missing-authorization-check pattern at the scale of an entire customer tenant on a shared platform, rather than a single object or user.