Skip to main content

Command Palette

Search for a command to run...

Preventing Broken Object Level Authorization (BOLA) in ASP.NET Core APIs

Updated
โ€ข11 min read
Preventing Broken Object Level Authorization (BOLA) in ASP.NET Core APIs

Broken Object Level Authorization โ€” BOLA for short โ€” sits at the top of the OWASP API Security Top 10 for good reason: it is the most exploited API vulnerability in production systems today. An authenticated user manipulates an object identifier in a request, the API performs the operation without confirming ownership, and suddenly user A is reading, modifying, or deleting user B's data. The authentication layer did its job; the authorization layer never showed up.

For teams building serious APIs, the patterns that prevent BOLA are well-understood and available in ASP.NET Core out of the box โ€” but knowing they exist and knowing when to reach for them are two different things. The complete implementation โ€” including resource-based authorization handlers, ownership checks wired to your domain model, and the test suite that proves they work โ€” is available on Patreon, with source code ready to adapt for your own services.

Understanding BOLA in context matters just as much as the fix. Chapter 8 of the ASP.NET Core Web API: Zero to Production course covers resource-based authorization using IAuthorizationService.AuthorizeAsync inside a complete production API โ€” with the full authorization pipeline connected, not just isolated snippets.

What Is BOLA and Why Does It Happen?

BOLA occurs when an API endpoint accepts an object identifier from the client โ€” a numeric ID, a UUID, a slug โ€” and uses it to fetch or modify a resource without verifying that the requesting user actually owns or has permission over that specific object.

The classic pattern looks harmless: a user sends GET /api/orders/4491 and receives the order. The problem surfaces when that same user changes the path to GET /api/orders/4490 and receives someone else's order without any error. The endpoint is correctly authenticated โ€” the user has a valid JWT โ€” but there is no check that order 4490 belongs to them.

BOLA is particularly dangerous because it looks correct from the outside. Standard monitoring and logging tools rarely flag it. Penetration testers find it by substituting IDs methodically. Attackers do the same at scale with automated tools.

The Root Cause: Authentication Is Not Authorization

The single most common reason BOLA exists in ASP.NET Core APIs is that teams conflate authentication with authorization. Adding [Authorize] to a controller confirms that the caller has a valid token. It says nothing about whether that caller has rights over the specific resource they are requesting.

There are three common manifestations of this failure:

Direct database lookup without ownership check. The controller takes the ID from the route, queries the database, and returns whatever comes back โ€” with no WHERE clause that constrains results to the current user's data.

Role-checked but not owner-checked. The endpoint verifies that the user is in the User role, which confirms they are a normal user rather than a guest. It does not verify that the resource at the given ID belongs to that specific user.

Policy-checked globally but not per resource. A policy confirms a claim exists โ€” for example, that the user has agreed to terms of service. That claim is static and session-level. It cannot tell you whether the user owns document 7731.

How ASP.NET Core Provides the Right Primitives

ASP.NET Core's authorization framework has exactly the right abstraction for BOLA prevention: resource-based authorization via IAuthorizationService. The key insight is that authorization decisions can and should be deferred until the resource is loaded, not evaluated before the handler runs.

The flow works like this: load the resource from the database, then call IAuthorizationService.AuthorizeAsync(user, resource, requirement) with the loaded entity as the second argument. The authorization handler receives both the ClaimsPrincipal and the actual domain object, which means it can compare the current user's identifier against the entity's owner field with full access to the object's state.

This is fundamentally different from attribute-based authorization, which operates before the handler runs and cannot inspect the resource at all.

What a Correct Ownership Check Looks Like

At the handler level, the check is straightforward: extract the user ID from the claims principal, compare it against the resource's owner identifier, and return either Succeed or Fail on the requirement. The handler has no side effects โ€” it does not throw, does not log, does not redirect. It makes a single binary decision.

The controller code that drives this pattern loads the resource first, then calls AuthorizeAsync before doing anything else with the loaded data. If authorization fails, the controller returns 403 Forbidden. The resource is never returned to the client, even partially.

One important subtlety: returning 404 Not Found instead of 403 Forbidden for unauthorized object access is a valid and often preferable design choice in APIs that should not reveal whether an object exists at all. This is a deliberate trade-off โ€” 404 leaks less information about your data model but can complicate debugging. The choice belongs to your threat model, not your controller logic.

Why Global Filters Cannot Solve This

A common attempted shortcut is to build a global action filter or middleware that automatically appends ownership conditions to every query. This approach sounds appealing but breaks down in practice:

Global ownership filters cannot know which foreign key represents "ownership" on every entity. Some resources have a direct UserId. Others are owned indirectly through a parent entity. Some are shared across an organization and should be visible to all members. Others are owned by role rather than by user identity. There is no general rule.

The correct level of abstraction is the authorization handler โ€” written once per resource type, applied explicitly at the point where the resource is loaded. It is more code than a global filter, but it is code that actually expresses your business rules rather than code that guesses at them.

Defence-in-Depth: What Else Belongs in the Stack

Resource-based ownership checks are the core defense, but BOLA prevention in a mature API involves several additional layers:

Use opaque identifiers where possible. Sequential integer IDs (/orders/4490, /orders/4491) invite enumeration. UUIDs or ULIDs do not prevent BOLA on their own โ€” an attacker can still try random UUIDs โ€” but they make brute-force enumeration impractical and reduce the attack surface meaningfully.

Apply tenant scoping at the query layer. In multi-tenant APIs, every database query against tenant-owned data should include the tenant ID in the WHERE clause, resolved from the authenticated user's claims โ€” not from the request payload. The tenant ID in the URL or body is advisory at best; the one in the token is authoritative.

Log authorization failures explicitly. A user hitting 403 once is probably testing their own edge cases. A user hitting 403 in a pattern across different object IDs is likely running an automated authorization probe. Structured logs with the user identity, the requested object ID, and a flag for authorization failure enable anomaly detection that generic APM tools miss.

Return consistent error shapes. Returning different error messages for "object not found" versus "object found but access denied" leaks information. IExceptionHandler or middleware-level error normalization ensures that every authorization rejection surfaces as an identical Problem Details response regardless of the internal reason.

Test authorization explicitly in your integration test suite. The most common BOLA regression pattern is a developer adding a new endpoint and forgetting the ownership check โ€” especially under deadline pressure. Integration tests with WebApplicationFactory that use two separate authenticated users and verify that user A cannot access user B's resources catch this class of bug reliably.

The OWASP API1:2023 Connection

BOLA is formally documented as API1:2023 in the OWASP API Security Top 10. The OWASP guidance is useful for communicating risk to stakeholders: this is not a theoretical vulnerability class. Real breaches in production APIs โ€” across healthcare, financial services, and SaaS platforms โ€” trace back to missing object-level authorization checks. The OWASP framing helps engineering leadership understand why resource-based authorization deserves explicit investment rather than being left as an implicit assumption.

What an Internal Code Review Should Check

When reviewing a pull request for BOLA exposure, the questions to ask are concrete:

Does every endpoint that accepts an object ID load the object from the database before performing any operation? If the object is not loaded, ownership cannot be verified.

After loading, does the handler call IAuthorizationService.AuthorizeAsync with the loaded resource? If it skips authorization and proceeds directly to the business logic, that is a gap.

Does the authorization handler compare the caller's user ID or tenant ID against the resource's owner field โ€” and is that comparison against a claim from the token, not a value from the request body?

Does the endpoint return 403 (or 404 if appropriate) immediately after a failed authorization check, with no partial response returned?

These four checks cover the vast majority of BOLA patterns. Building them into PR template checklists, automated linting rules that flag repository calls without a following authorization check, or architecture fitness functions reduces the surface area sustainably.


โ˜• Prefer a one-time tip? Buy us a coffee โ€” every bit helps keep the content coming!

Frequently Asked Questions

What is the difference between BOLA and IDOR?

IDOR (Insecure Direct Object Reference) is an older term that describes the same class of vulnerability. OWASP replaced IDOR with BOLA in their API Security Top 10 to emphasize that the core problem is a broken authorization decision at the object level, not just the use of a direct reference. In practice, the terms are used interchangeably and refer to the same attack pattern: accessing or modifying a resource by manipulating its identifier without the API enforcing ownership.

Does adding [Authorize] to a controller prevent BOLA?

No. [Authorize] confirms that the caller has a valid, unexpired token โ€” it verifies authentication. BOLA is an authorization failure, not an authentication failure. The user is authenticated; the missing check is whether that authenticated user has rights over the specific object identified in the request. Resource-based authorization via IAuthorizationService.AuthorizeAsync is the correct mechanism for that check.

Should I return 403 Forbidden or 404 Not Found when authorization fails?

Both are defensible choices. Returning 403 is explicit โ€” it tells the caller they were denied, but their request was understood. Returning 404 is a security-through-obscurity technique that avoids revealing whether an object with the given ID even exists. For APIs where the existence of a resource is itself sensitive information, 404 reduces information leakage. For APIs where resource existence is not sensitive, 403 is more precise and easier to debug. The important thing is to be consistent โ€” mixing the two approaches leaks patterns to an attacker.

Can a global filter or middleware prevent BOLA automatically?

Not reliably. Global filters operate before the resource is loaded and cannot inspect the resource's ownership fields. A global approach that appends ownership conditions to every query requires intimate knowledge of every entity's ownership model and breaks down for shared resources, hierarchical ownership, and role-owned objects. The correct level of abstraction is a per-resource authorization handler registered through the ASP.NET Core authorization framework, applied explicitly at the point where each resource is loaded.

How do I test for BOLA vulnerabilities in my ASP.NET Core API?

The most reliable approach is integration testing with WebApplicationFactory. Create two distinct test users, authenticate both, and verify with explicit assertions that user A cannot access, modify, or delete resources owned by user B โ€” and that the correct HTTP status code (403 or 404) is returned. Automated security scanners such as OWASP ZAP can supplement manual testing, but they cannot replace explicit integration tests that encode your authorization rules as verifiable expectations.

Does using UUIDs instead of sequential integers prevent BOLA?

No โ€” UUIDs are not a security control for BOLA. Using UUIDs reduces the practicality of sequential enumeration attacks, which is a meaningful defence-in-depth improvement. But if a UUID is ever leaked โ€” through a log entry, a shared link, or a response body โ€” an attacker can use it directly. Ownership checks must be present regardless of the identifier type used.

Where does BOLA fit in the OWASP API Security Top 10?

BOLA is API1:2023 โ€” the number one ranked API security risk in the OWASP API Security Top 10 for 2023. OWASP ranks it first because of its frequency, exploitability, and impact across real-world APIs. It is consistently found in security audits of APIs across every industry vertical, including healthcare, fintech, and multi-tenant SaaS platforms.