Web Security | OWASP Top 10 | Difficulty: Intermediate to Advanced

Broken Access Control Explained: A Deep Technical Guide to Authorization, IDOR, and Privilege Escalation

Published on: August 14, 2026

Broken Access Control is one of the most important security problems in modern web applications and APIs. In the OWASP Top 10: 2025, Broken Access Control is listed as A01:2025, placing it at the top of the current OWASP Top 10 web application risk list.

The reason is straightforward: an application can have strong encryption, secure passwords, multi-factor authentication, and carefully protected sessions, yet still expose sensitive information or dangerous functionality if it fails to correctly determine what an authenticated or unauthenticated user is actually allowed to do.

Access control is the security layer that answers questions such as:

  • Can this user access this resource?
  • Can this user modify this record?
  • Can this user delete this object?
  • Can this employee access another department's data?
  • Can this normal user access an administrator function?
  • Can this API client modify a property it should only be able to read?
  • Can this user perform an action on behalf of another user?
  • Can this member access a resource belonging to another organization?

When those decisions are missing, inconsistent, or incorrectly implemented, the result can be Broken Access Control.


What Is Broken Access Control?

Broken Access Control occurs when an application fails to properly enforce restrictions governing which users, services, or other principals are allowed to access resources or perform actions.

In simple terms:

The application knows who you are, but fails to correctly determine what you are allowed to do.

Access control is broader than simply protecting an /admin page.

It applies to:

  • Pages
  • API endpoints
  • Database records
  • Files
  • Objects
  • Functions
  • Administrative operations
  • Individual fields
  • Business operations
  • Tenant-specific resources
  • Background jobs
  • Static resources
  • Download endpoints
  • Internal APIs

OWASP's authorization guidance emphasizes that authorization is distinct from authentication and that being authenticated does not mean a user is authorized to perform every operation available to the application.


Authentication vs Authorization

One of the most important concepts in application security is understanding the difference between authentication and authorization.

Authentication

Authentication answers:

Who are you?

Examples include:

  • Username and password
  • Passkeys
  • Session cookies
  • OAuth authentication
  • OpenID Connect
  • Multi-factor authentication
  • Client certificates
  • Access tokens

For example:

User submits credentials
        ↓
Authentication system verifies them
        ↓
User identity established
        ↓
user_id = 1842

The application now knows that the requester is user 1842.

But that does not mean user 1842 can access every resource.


Authorization

Authorization answers:

What are you allowed to do?

For example:

User: 1842
Role: Customer
Organization: Company-A

The application may determine:

View own orders       → ALLOW
Edit own profile      → ALLOW
View another user's order → DENY
Delete another user's account → DENY
Access admin panel    → DENY

Authentication establishes identity.

Authorization establishes permissions.

A useful way to remember the difference is:

Authentication = Who are you?

Authorization = What are you allowed to do?

An application that correctly authenticates users but incorrectly implements authorization can still be severely vulnerable.


Why Broken Access Control Is So Dangerous

Access control sits directly between a user and the application's resources and functionality.

If the control fails, attackers may be able to access things that were never intended for them.

Depending on the application, this could result in:

  • Unauthorized data access
  • Modification of another user's information
  • Account takeover chains
  • Privilege escalation
  • Unauthorized administrative actions
  • Exposure of confidential files
  • Unauthorized financial operations
  • Cross-tenant data access
  • Deletion of resources
  • Manipulation of business records
  • Access to internal functionality

The impact depends on what the vulnerable account can reach and what actions the application permits.

A vulnerability allowing a user to read another user's public profile is very different from one allowing a normal user to delete every account in the system.


The Core Access Control Question

Every sensitive request should ultimately answer a question similar to:

Who is making this request?

        +

What action are they trying to perform?

        +

What resource are they trying to access?

        +

What properties or context apply?

        ↓

Is this operation authorized?

For example:

User A
   |
   | DELETE /api/orders/123
   v
Authorization Layer
   |
   +-- Is User A authenticated?
   |
   +-- Is User A allowed to delete orders?
   |
   +-- Does order 123 belong to User A?
   |
   +-- Is the order in a state where deletion is allowed?
   |
   v
ALLOW / DENY

Authorization can therefore involve much more than checking a single role.


IDOR: Insecure Direct Object Reference

One of the best-known examples of Broken Access Control is Insecure Direct Object Reference (IDOR).

IDOR occurs when an application exposes an object identifier and uses it to retrieve or modify a resource without properly verifying whether the requester is authorized to access that particular object.

In modern API security terminology, this is closely associated with Broken Object Level Authorization (BOLA). OWASP lists BOLA as API1:2023 in the OWASP API Security Top 10 and recommends object-level authorization checks whenever an API function accesses data using an identifier supplied by the client.


IDOR Example: Viewing Another User's Order

Imagine an e-commerce application.

A customer requests:

GET /api/orders/10521 HTTP/1.1
Host: example.com
Authorization: Bearer <token>

The server retrieves order 10521.

If that order belongs to the authenticated user, everything is fine.

Now suppose the same user changes the identifier:

GET /api/orders/10522 HTTP/1.1
Host: example.com
Authorization: Bearer <token>

If order 10522 belongs to another customer and the server returns it anyway, the application has an object-level authorization failure.

The problem is not that the attacker guessed an ID.

The actual problem is:

The server trusted the object identifier without verifying authorization for that object.

Why Random IDs Do Not Fix IDOR

A common misconception is that replacing sequential IDs with UUIDs completely solves IDOR.

It does not.

Suppose the application changes:

/api/orders/10521

to:

/api/orders/550e8400-e29b-41d4-a716-446655440000

The identifier is now harder to guess.

But if an attacker obtains another valid identifier through:

  • A leaked response
  • A shared link
  • Browser history
  • Logs
  • Referrer leakage
  • Another API response
  • Client-side application data
  • A compromised account

and the server still fails to check authorization, the vulnerability remains.

Therefore:

Unpredictable identifiers are defense in depth, not an authorization mechanism.

OWASP specifically recommends ensuring that resources cannot be accessed merely because their lookup identifiers are known or guessed.


A Better Server-Side Pattern

Consider an application where every order belongs to a user.

A basic implementation might look like:

const order = await db.orders.findById(req.params.orderId);

if (!order) {
    return res.status(404).send("Not Found");
}

if (order.userId !== req.user.id) {
    return res.status(403).send("Forbidden");
}

return res.json(order);

The important security property is not the JavaScript itself.

The important property is that the server evaluates authorization against the actual resource.

A more complex application might not use simple ownership.

For example:

User
  |
  +-- Organization membership
  |
  +-- Project membership
  |
  +-- Role
  |
  +-- Resource relationship
  |
  +-- Specific permission

Authorization therefore needs to match the application's actual business rules.


Horizontal Privilege Escalation

Horizontal privilege escalation occurs when a user accesses resources or functionality belonging to another user with a similar privilege level.

Example:

User A
  |
  | Request own profile
  v
/api/users/1001

This is allowed.

But:

User A
  |
  | Change ID
  v
/api/users/1002

If user 1002 is another customer and the server returns private information, User A has crossed a horizontal authorization boundary.

Both users might have the same role:

Customer

The difference is which resources they are allowed to access.

This is why role checks alone cannot solve all access-control problems.


Vertical Privilege Escalation

Vertical privilege escalation occurs when a lower-privileged user accesses functionality intended for a higher-privileged user.

For example:

Administrator
    |
    +-- Delete users
    +-- Modify billing
    +-- View security logs
    +-- Change system configuration

Normal User
    |
    +-- View own profile
    +-- Edit own profile

If a normal user sends:

DELETE /api/admin/users/1842 HTTP/1.1
Host: example.com
Authorization: Bearer <normal-user-token>

and the server performs the operation, the application has a function-level authorization failure.


Function-Level Authorization

Function-level authorization controls which operations a principal is allowed to perform.

For example:

GET    /api/profile
PATCH  /api/profile
DELETE /api/account

might be available to normal users.

While:

GET    /api/admin/users
DELETE /api/admin/users/:id
POST   /api/admin/roles

might require administrative permissions.

The critical point is that the server must enforce those permissions.

Simply hiding the administrator interface from ordinary users is not sufficient.


Hidden Links Are Not Security Controls

Consider:

if (user.role === "admin") {
    showAdminPanel();
}

This controls what the browser displays.

It does not secure the endpoint.

An attacker can ignore the interface entirely and directly send:

GET /api/admin/dashboard

The server must independently evaluate authorization.

The correct architecture is:

Client
   |
   | Request
   v
Server
   |
   | Authentication
   v
Identity
   |
   | Authorization
   v
Permission Decision
   |
   +---- ALLOW → Execute operation
   |
   +---- DENY  → Reject request

OWASP recommends forcing requests through access-control checks unless the resource is intentionally public, and enforcing authorization server-side.


Forced Browsing

Forced browsing refers to accessing application resources directly when the normal interface does not expose or link to them.

For example, a regular user might never see:

/admin/reports

But that does not make the endpoint secure.

If the server returns the report whenever someone requests the URL, the application has an authorization problem.

Security must exist at the server-side resource boundary.

Not at the navigation layer.


API Access Control

Broken access control is particularly important for APIs.

Modern applications commonly expose endpoints such as:

GET    /api/users/:id
GET    /api/orders/:id
PATCH  /api/orders/:id
DELETE /api/files/:id
POST   /api/projects/:id/members
GET    /api/organizations/:id/billing

Every endpoint can introduce an authorization decision.

For example:

GET /api/orders/123

might require:

Authenticated?
        ↓
Has order:read permission?
        ↓
Does order 123 belong to user's organization?
        ↓
Is user a member of that organization?
        ↓
Is the order visible to this user?
        ↓
ALLOW

A valid access token alone is not enough.


A Valid JWT Does Not Mean Full Authorization

This is a common API security mistake.

Suppose a JWT contains:

{
  "sub": "1842",
  "role": "user"
}

The token proves that the server has an authenticated identity associated with the request, assuming the token itself is valid.

It does not automatically mean the user can access:

/api/users/9999

or:

/api/admin/settings

or:

/api/organizations/another-company/billing

The server must still evaluate the requested operation against the application's authorization policy.


Broken Object Property Level Authorization

Authorization can fail at a level smaller than the object itself.

Imagine:

{
  "name": "John",
  "email": "[email protected]",
  "role": "user"
}

Suppose a normal user is allowed to update their name and email but not their role.

A dangerous API design might accept:

PATCH /api/users/1842
Content-Type: application/json

with:

{
  "name": "John",
  "role": "admin"
}

If the backend blindly applies every supplied property, the user may modify a field they are not authorized to change.

This is why authorization sometimes needs to be evaluated at the property or field level, not merely at the object level.

OWASP's API Security Top 10 includes Broken Object Property Level Authorization as API3:2023.


Multi-Tenant Access Control

Multi-tenant applications introduce another major authorization challenge.

Imagine a SaaS platform containing:

Organization A
 ├── User 1
 ├── User 2
 └── Projects

Organization B
 ├── User 3
 ├── User 4
 └── Projects

A user belonging to Organization A should generally not be able to access Organization B's data.

A dangerous implementation might do:

SELECT * FROM invoices WHERE id = ?

without checking which organization owns the invoice.

A safer design must incorporate tenant boundaries into authorization.

Conceptually:

User
  ↓
Organization membership
  ↓
Resource ownership / relationship
  ↓
Requested action

For example:

SELECT *
FROM invoices
WHERE id = ?
AND organization_id = current_user.organization_id

The exact implementation depends on the architecture, but the security principle is the same:

Tenant boundaries must be enforced server-side.

Role-Based Access Control

Role-Based Access Control (RBAC) assigns permissions through roles.

For example:

User
 └── Role: Editor
       ├── article:read
       ├── article:create
       └── article:update

Another user might have:

Role: Viewer
    └── article:read

RBAC can be useful, especially for relatively stable permission models.

However, roles alone may become insufficient when authorization depends on relationships or contextual information.

For example:

Can Alice edit Project X?

might depend on:

  • Alice's role
  • Project membership
  • Organization membership
  • Project ownership
  • Project status
  • Resource sensitivity
  • Current action

This is why modern authorization systems may also use attribute-based or relationship-based access control. OWASP's authorization guidance specifically discusses ABAC and ReBAC as useful approaches for more fine-grained authorization decisions.


Attribute-Based Access Control

Attribute-Based Access Control (ABAC) makes authorization decisions using attributes.

Attributes can include:

User role
Department
Organization
Resource owner
Resource classification
Device state
Request context
Action
Environment

For example:

Allow access if:

user.organization == resource.organization

AND

user.department == resource.department

AND

user.permission contains "read"

AND

resource.classification != "restricted"

ABAC can express policies that are difficult to represent using simple role checks.


Relationship-Based Access Control

Relationship-Based Access Control, or ReBAC, considers relationships between entities.

For example:

Alice
  |
  +-- member_of --> Engineering
  |
  +-- member_of --> Project-A

Then:

Project-A
  |
  +-- contains --> Repository-X

The authorization decision may be:

Alice can read Repository-X
because Alice is a member of Project-A
and Project-A owns Repository-X.

This type of relationship becomes particularly useful in collaborative SaaS platforms, messaging applications, project-management systems, document platforms, and other systems where permissions depend on relationships rather than simple global roles.


Least Privilege

The principle of least privilege means giving users, services, and processes only the permissions required to perform their intended tasks.

For example:

Customer
→ Read own orders

Support Agent
→ Read customer support records

Manager
→ Approve certain business operations

Administrator
→ Manage system configuration

Do not give every authenticated user broad permissions simply because the application is easier to implement that way.

OWASP recommends applying least privilege both horizontally and vertically.


Deny by Default

A strong authorization architecture follows:

If access has not been explicitly allowed, deny it.

For example:

Request
   |
   v
Authorization policy
   |
   +---- Explicitly allowed → Continue
   |
   +---- No matching permission → Deny
   |
   +---- Authorization error → Deny

The dangerous alternative is:

If permission check fails:
    continue anyway

Authorization failures should not silently become successful requests.

OWASP specifically recommends deny-by-default and notes that access-control decisions should be explicit rather than relying on permissive defaults.


Authorization Must Be Enforced on Every Sensitive Request

An application should not assume that authorization was already checked somewhere else.

For sensitive operations, authorization should be evaluated at the appropriate server-side boundary.

For example:

POST /api/payments

should have an authorization decision.

So should:

GET /api/payments/123

and:

DELETE /api/payments/123

These are different operations and may have different permissions.

A user who can read a resource should not automatically be able to modify or delete it.


Read, Write, Update, and Delete Are Different Permissions

Consider:

Document

A user may have:

document:read

but not:

document:update
document:delete
document:share
document:publish

Authorization should reflect the application's actual actions.

Do not assume:

Can read = Can modify

or:

Can modify = Can delete

Those permissions should be intentionally designed.


HTTP Methods Do Not Automatically Provide Authorization

Another common mistake is assuming that using different HTTP methods automatically creates security boundaries.

For example:

GET    /api/users/123
PATCH  /api/users/123
DELETE /api/users/123

The framework may distinguish these methods technically.

But authorization still needs to determine whether the requester can perform each operation.

A user might be allowed to:

GET /api/users/123

while being denied:

DELETE /api/users/123

HTTP 401 vs 403

Access-control implementations frequently use two HTTP status codes.

401 Unauthorized

Generally indicates that the request does not contain valid authentication credentials.

Example:

No valid session
        ↓
401 Unauthorized

403 Forbidden

Generally indicates that the server knows the requester but refuses the requested operation.

Example:

Authenticated normal user
        ↓
Attempts admin operation
        ↓
403 Forbidden

The exact response strategy can vary by application, but developers should distinguish authentication failures from authorization failures consistently.


Do Not Trust Client-Supplied Roles

Never rely solely on values such as:

{
  "role": "admin"
}

sent by the client.

Likewise, do not trust:

isAdmin=true

or:

permissions=["admin"]

simply because they came from the browser or mobile application.

The server must obtain and validate authorization information from a trusted source.

Client-side values can be modified.

The server is the security boundary.


Client-Side Authorization Is Not Authorization

This is insecure:

if (user.role === "admin") {
    enableDeleteButton();
}

The UI can improve user experience, but it should never be the only security control.

An attacker can bypass the UI completely.

For example:

Browser
   |
   X Normal UI
   |
   v
Direct API request

Therefore:

Frontend authorization = UX

Server-side authorization = Security

Common Broken Access Control Patterns

Several patterns repeatedly cause authorization vulnerabilities.

Pattern 1: Check Authentication but Not Authorization

if (user.isAuthenticated) {
    return resource;
}

This answers:

Is the user logged in?

It does not answer:

Is the user allowed to access this resource?

Pattern 2: Trust the Object ID

GET /api/orders/123

The application retrieves order 123 without checking ownership or another authorization relationship.

Pattern 3: Hide Administrative URLs

/admin

is removed from the UI, but the endpoint remains accessible to ordinary users.

Pattern 4: Trust Client-Supplied Roles

{
  "role": "admin"
}

is accepted without server-side validation.

Pattern 5: Authorize Only on the Frontend

The frontend blocks a button, but the backend accepts the request anyway.

Pattern 6: One Authorization Check for Everything

A user gets a broad permission such as:

user:write

and the application assumes that means the user can modify every user resource.

Authorization often needs to be resource- and action-specific.

Pattern 7: Fail Open

A serious authorization bug can occur when an error results in access being granted.

For example:

Authorization service unavailable
        ↓
Application continues
        ↓
Access granted

A security-sensitive authorization failure should generally fail closed rather than granting access.


Static Resources Need Authorization Too

Access control is not limited to dynamic API endpoints.

Consider:

GET /uploads/private-report.pdf

If the file is private, simply hiding the download link is insufficient.

The server or storage layer must enforce access to the resource.

OWASP's authorization guidance explicitly calls out the need to enforce authorization for static resources as well.


Background Jobs and Internal APIs

Authorization problems can also exist outside normal browser requests.

For example:

Web API
   ↓
Message Queue
   ↓
Worker
   ↓
Database

If a user can influence a job containing:

user_id
resource_id
action

the worker must not blindly trust those values.

Security decisions must remain valid across asynchronous processing boundaries.

The same principle applies to internal APIs.

A service being "internal" does not automatically mean every other service should be allowed to perform every operation.


Microservices and Access Control

Microservice architectures can make authorization more complicated.

Consider:

API Gateway
     |
     +---- User Service
     |
     +---- Order Service
     |
     +---- Payment Service
     |
     +---- File Service

A common mistake is assuming that because the API gateway authenticated the user, every downstream service can blindly trust the request.

Each service needs to understand what security guarantees it receives from upstream components and what authorization it must enforce itself.

Authentication and authorization responsibilities should be explicitly designed across service boundaries.


Multi-Step Business Operations

Access control can also fail when an operation involves multiple steps.

For example:

Create payment
    ↓
Confirm payment
    ↓
Refund payment

A user might be authorized to create a payment but not refund it.

Authorization should therefore consider the specific business action, not merely whether the user has access to the general resource.


Business Logic and Access Control

Not every access-control vulnerability looks like:

GET /admin

Sometimes the application correctly verifies that the user owns the resource but fails to verify whether the requested action is allowed in the current state.

For example:

Order status = shipped

A customer might own the order but still not be authorized to perform:

Change shipping address

The application therefore needs to evaluate both:

Who owns the resource?

and:

Is this action permitted in this context?

This is why access control is closely connected to business logic.


How to Design Access Control Correctly

A good authorization architecture begins before the code is written.

Step 1: Identify Actors

Define who can interact with the system.

Examples:

Anonymous visitor
Customer
Employee
Manager
Administrator
Service account
Internal service

Step 2: Identify Resources

List what needs protection.

Examples:

Users
Orders
Invoices
Files
Projects
Messages
Payments
API keys
System settings

Step 3: Identify Actions

Define operations explicitly.

Examples:

read
create
update
delete
share
publish
approve
refund
export

Step 4: Define Relationships

Determine relationships such as:

User → owns → Order

User → member_of → Organization

User → member_of → Project

Manager → manages → Team

Step 5: Define Policies

Then determine which actors can perform which actions against which resources.

Example:

Customer:
  read own orders
  create own orders

Support:
  read assigned customer records

Manager:
  approve assigned operations

Administrator:
  manage system configuration

Centralize Authorization Logic

Authorization logic should be consistent and reusable.

Instead of writing different authorization assumptions throughout hundreds of endpoints, establish well-defined policies or authorization services.

For example:

authorize(user, action, resource)

might conceptually return:

ALLOW

or:

DENY

The implementation can be much more sophisticated than this example, but the principle is to make authorization decisions explicit and testable.

OWASP recommends carefully reviewing authorization logic and testing it rather than assuming a framework or library automatically implements the application's required security model.


Authorization Testing

Access-control testing should be part of the development lifecycle.

A useful test matrix might look like:

User Resource Action Expected
Owner Own order Read Allow
Owner Own order Update Allow
Owner Other user's order Read Deny
Owner Other user's order Update Deny
Normal user Admin function Execute Deny
Admin User management Read Allow
Admin User management Delete Allow

The exact matrix depends on the application.

The important point is to test both:

Allowed operations

and:

Forbidden operations

Test More Than One User

Authorization bugs are often invisible when testing with a single account.

A proper test environment should contain accounts representing different security contexts.

For example:

User A
User B
Manager
Administrator
Organization A member
Organization B member

Then test cross-user and cross-tenant access.

This is especially important for API endpoints containing object identifiers.

OWASP recommends unit and integration testing of authorization logic, while recognizing that automated tests do not replace skilled security testing.


Authorization Test Cases

For an endpoint such as:

GET /api/orders/:id

tests should include:

Authenticated owner
→ Allow

Authenticated non-owner
→ Deny

Unauthenticated user
→ Deny if resource is private

Administrator
→ Allow if policy permits

User from another tenant
→ Deny

Deleted resource
→ Appropriate denial/not-found behavior

For:

DELETE /api/users/:id

test:

Normal user
→ Deny

Manager
→ Deny unless explicitly allowed

Administrator
→ Allow if authorized

Unauthenticated request
→ Deny

Security Logging

Authorization decisions can also be valuable security events.

Depending on the application, useful logging may include:

User identity
Requested resource
Requested action
Authorization result
Reason for denial
Timestamp
Request identifier
Relevant tenant/context

Avoid logging sensitive information unnecessarily.

Authorization logs can help identify patterns such as:

User repeatedly attempting to access another tenant's resources

or:

Normal user repeatedly requesting administrative endpoints

OWASP's authorization guidance recommends appropriate authorization logging.


Do Not Leak Excessive Authorization Information

Applications should also consider how denial responses reveal information.

For example:

GET /api/users/9999

might return:

404 Not Found

while an existing but unauthorized resource might return:

403 Forbidden

Whether an application chooses 403 or an existence-hiding 404 depends on its security and API design requirements.

In some applications, exposing whether an object exists can itself leak sensitive information.

The important point is to deliberately design the response behavior rather than treating status codes as an afterthought.


Access Control and Caching

Caching can create unusual authorization problems.

Imagine:

User A requests private resource
        ↓
Server generates response
        ↓
Shared cache stores response
        ↓
User B requests same URL

If the response was incorrectly cached as public, User B might receive User A's private information.

Therefore, authorization must be considered alongside:

  • HTTP caching
  • CDN configuration
  • Reverse proxies
  • Browser caching
  • Shared object storage
  • API gateways

Access control is not complete if an authorized response can later be served to an unauthorized user.


Access Control and File Storage

Private files should not simply be placed in publicly accessible directories.

For example:

/public/uploads/private-report.pdf

may accidentally expose the file regardless of application-level authorization.

A better design may use:

Private Storage
       ↓
Authorization Check
       ↓
Short-lived authorized access

The exact architecture varies, but the security requirement remains:

A user must not be able to bypass application authorization by accessing the underlying storage directly.

Access Control in APIs vs Traditional Web Pages

Traditional web applications might protect:

/admin
/account
/settings

Modern applications also need to protect granular API operations:

/api/users/123
/api/projects/456
/api/files/789
/api/payments/321

This makes object-level authorization especially important.

OWASP's API Security Top 10 specifically identifies BOLA as API1:2023 and Broken Function Level Authorization as API5:2023.


Common Misconceptions

"The user is logged in, so access is safe."

False.

Authentication does not establish authorization for every resource.

"UUIDs prevent IDOR."

False.

They make identifiers harder to guess but do not replace authorization.

"The admin URL is hidden."

Irrelevant.

An attacker can send a direct request.

"The frontend checks the role."

Not sufficient.

The backend must enforce authorization.

"The endpoint is internal."

Not automatically safe.

Internal services still need appropriate authentication and authorization.

"The user owns the object, so they can perform anything."

Not necessarily.

Ownership and action-specific permissions can be different.

"RBAC solves access control."

Not always.

Complex applications may require object relationships, attributes, resource ownership, tenant boundaries, and contextual policies.


A Secure Authorization Model

A mature application can think about authorization as:

                  REQUEST
                     |
                     v
              Authentication
                     |
                     v
              Identify Subject
                     |
                     v
        +---------------------------+
        | Authorization Policy      |
        |                           |
        | Subject                   |
        | Action                    |
        | Resource                  |
        | Relationship              |
        | Attributes                |
        | Context                   |
        +---------------------------+
                     |
              +------+------+
              |             |
            ALLOW          DENY
              |             |
              v             v
        Execute Action   Reject Request

The security decision should be based on the actual policy rather than assumptions made by the client.


Defense-in-Depth Strategy

There is no single control that solves every access-control problem.

A strong application combines multiple layers:

Authentication
      ↓
Session / Token Validation
      ↓
Authorization
      ↓
Object-Level Permission
      ↓
Function-Level Permission
      ↓
Property-Level Permission
      ↓
Tenant / Relationship Check
      ↓
Business Rule Validation
      ↓
Database / Storage Boundary
      ↓
Logging and Monitoring

Each layer addresses a different part of the security model.


Broken Access Control Security Checklist

Before deploying a web application or API, review the following:

  • [ ] Authentication and authorization are treated as separate security controls.
  • [ ] Authorization is enforced server-side.
  • [ ] Access is denied by default.
  • [ ] Least privilege is applied.
  • [ ] Sensitive requests pass through authorization checks.
  • [ ] Object-level authorization is enforced.
  • [ ] Function-level authorization is enforced.
  • [ ] Property/field-level authorization is considered where necessary.
  • [ ] Horizontal privilege escalation has been tested.
  • [ ] Vertical privilege escalation has been tested.
  • [ ] Cross-tenant access has been tested.
  • [ ] Ownership checks are enforced server-side.
  • [ ] Client-supplied roles are not blindly trusted.
  • [ ] Client-side checks are not treated as security controls.
  • [ ] Hidden URLs are not relied upon for protection.
  • [ ] UUIDs or random IDs are not treated as authorization controls.
  • [ ] Administrative endpoints require explicit authorization.
  • [ ] Private static resources are protected.
  • [ ] File-storage permissions are correctly configured.
  • [ ] Background jobs enforce appropriate authorization boundaries.
  • [ ] Internal APIs have appropriate access controls.
  • [ ] Business actions have action-specific authorization.
  • [ ] Authorization failures do not fail open.
  • [ ] Authorization logic is centralized or consistently implemented.
  • [ ] Authorization policies are covered by automated tests.
  • [ ] Multiple user roles are included in security testing.
  • [ ] Cross-user access tests are performed.
  • [ ] Cross-tenant access tests are performed where applicable.
  • [ ] Authorization events are logged appropriately.
  • [ ] Caching does not expose authorized responses to unauthorized users.

Final Takeaway

Broken Access Control is fundamentally a failure to correctly enforce who can perform what action against which resource under which conditions.

The mistake is often much simpler than the resulting impact.

An application may correctly authenticate:

User A

but then fail to ask:

Is User A allowed to access Resource B?

Or it may correctly determine that User A owns a resource but fail to ask:

Is User A allowed to perform this particular action?

That distinction is where many serious authorization vulnerabilities begin.

A secure application therefore needs to treat authorization as a first-class security requirement rather than as a collection of UI restrictions.

The server must enforce the policy.

Object ownership must be verified.

Administrative functions must be protected.

Tenant boundaries must be enforced.

Sensitive fields must have appropriate permissions.

Business operations must have explicit authorization rules.

And authorization decisions must fail safely when the application cannot establish that access is permitted.

The most important principle is simple:

Never assume that because a user is authenticated, they are authorized to access or modify a particular resource.

Authentication establishes identity.

Authorization establishes permission.

Broken Access Control happens when the second part is missing, incomplete, inconsistent, or incorrectly enforced.

For modern APIs, this is particularly important because object identifiers, functions, properties, and business operations are exposed directly through endpoints. OWASP's API Security Top 10 therefore treats Broken Object Level Authorization and Broken Function Level Authorization as distinct major API risks.

For developers, the practical rule is equally simple:

Authenticate the requester.
Identify the requested action.
Identify the requested resource.
Evaluate the authorization policy.
Deny by default.
Allow only what the policy explicitly permits.

That approach is far more reliable than hiding URLs, relying on frontend controls, using unpredictable IDs, or assuming that an authenticated user can safely access everything exposed by the application.