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:
When those decisions are missing, inconsistent, or incorrectly implemented, the result can be 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:
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.
One of the most important concepts in application security is understanding the difference between authentication and authorization.
Authentication answers:
Who are you?
Examples include:
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 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.
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:
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.
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.
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.
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.
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:
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.
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 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 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 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.
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 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.
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.
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.
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 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 (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:
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 (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, 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.
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.
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.
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.
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.
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
Access-control implementations frequently use two HTTP status codes.
Generally indicates that the request does not contain valid authentication credentials.
Example:
No valid session
↓
401 Unauthorized
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.
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.
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
Several patterns repeatedly cause authorization vulnerabilities.
if (user.isAuthenticated) {
return resource;
}
This answers:
Is the user logged in?
It does not answer:
Is the user allowed to access this resource?
GET /api/orders/123
The application retrieves order 123 without checking ownership or another
authorization relationship.
/admin
is removed from the UI, but the endpoint remains accessible to ordinary users.
{
"role": "admin"
}
is accepted without server-side validation.
The frontend blocks a button, but the backend accepts the request anyway.
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.
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.
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.
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.
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.
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.
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.
A good authorization architecture begins before the code is written.
Define who can interact with the system.
Examples:
Anonymous visitor
Customer
Employee
Manager
Administrator
Service account
Internal service
List what needs protection.
Examples:
Users
Orders
Invoices
Files
Projects
Messages
Payments
API keys
System settings
Define operations explicitly.
Examples:
read
create
update
delete
share
publish
approve
refund
export
Determine relationships such as:
User → owns → Order
User → member_of → Organization
User → member_of → Project
Manager → manages → Team
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
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.
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
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.
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
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.
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.
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:
Access control is not complete if an authorized response can later be served to an unauthorized user.
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.
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.
False.
Authentication does not establish authorization for every resource.
False.
They make identifiers harder to guess but do not replace authorization.
Irrelevant.
An attacker can send a direct request.
Not sufficient.
The backend must enforce authorization.
Not automatically safe.
Internal services still need appropriate authentication and authorization.
Not necessarily.
Ownership and action-specific permissions can be different.
Not always.
Complex applications may require object relationships, attributes, resource ownership, tenant boundaries, and contextual policies.
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.
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.
Before deploying a web application or API, review the following:
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.