What is Broken Access Control / IDOR?
Broken Access Control is the #1 vulnerability in the OWASP Top 10, representing failures in enforcing restrictions on what authenticated users are allowed to do. Insecure Direct Object Reference (IDOR) is a specific and extremely common subtype: the application exposes internal object identifiers (database IDs, filenames, UUIDs) and allows users to access any record simply by changing that identifier.
A classic example: a user views their invoice at /api/invoices/1042. By changing 1042 to 1041, they retrieve another user’s invoice — because the server never checks whether the requester owns that record.
Beyond IDOR, broken access control includes vertical privilege escalation (a regular user accessing admin-only functionality), path traversal, and missing function-level access controls on API endpoints.
How exploitation works
# Victim's request
GET /api/orders/99201 HTTP/1.1
Authorization: Bearer <victim_token>
→ Returns victim's order
# Attacker increments the ID
GET /api/orders/99200 HTTP/1.1
Authorization: Bearer <attacker_token>
→ Returns ANOTHER user's order — no ownership check performed
Automated IDOR discovery tools enumerate IDs in bulk, extracting thousands of records in minutes if no rate limiting or ownership enforcement is in place.
Vulnerable code examples
C# / ASP.NET Core
// VULNERABLE: No ownership check — any authenticated user can fetch any order
[HttpGet("orders/{id}")]
[Authorize]
public async Task<IActionResult> GetOrder(int id)
{
var order = await _db.Orders.FindAsync(id);
return Ok(order); // Missing: verify order.UserId == currentUserId
}
Java / Spring
// VULNERABLE: Admin endpoint accessible to any authenticated user
@GetMapping("/api/admin/users/{id}")
@PreAuthorize("isAuthenticated()") // Should be hasRole('ADMIN')
public ResponseEntity<User> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userRepository.findById(id).orElseThrow());
}
Secure code examples
C# / ASP.NET Core — ownership enforcement
// SECURE: Explicitly verify the caller owns the requested resource
[HttpGet("orders/{id}")]
[Authorize]
public async Task<IActionResult> GetOrder(int id)
{
var currentUserId = User.GetUserId();
var order = await _db.Orders
.FirstOrDefaultAsync(o => o.Id == id && o.UserId == currentUserId);
if (order == null) return NotFound(); // Returns 404, not 403, to avoid enumeration
return Ok(order);
}
Python / Django — query scoped to current user
# SECURE: Filter queryset by the authenticated user — can never access others' data
@login_required
def get_document(request, doc_id):
doc = get_object_or_404(Document, id=doc_id, owner=request.user)
return JsonResponse(doc.to_dict())
What Offensive360 detects
- Unguarded object lookups — Database queries using user-supplied IDs without an ownership or role condition
- Missing role checks — Endpoints decorated with generic
[Authorize]/isAuthenticated()that should require specific roles or permissions - Direct file access — File download/read operations using user-supplied filenames or paths without access verification
- Insecure admin routes — Administrative endpoints with insufficient authorization annotations
- UUID-only security — Objects “protected” only by UUID obscurity, with no actual permission check
Remediation guidance
-
Enforce ownership at the data layer — Every query that retrieves user-specific data must include the authenticated user’s ID as a filter condition.
-
Apply role-based authorization to all endpoints — Do not rely on hidden UI elements to restrict access; verify authorization in the server-side handler.
-
Use indirect references — Map internal IDs to session-scoped tokens (e.g., a UUID that is only valid within the current session) to make enumeration harder.
-
Deny by default — Access control should default to deny. Explicitly grant permissions rather than implicitly allowing access.
-
Log and alert on access violations — Detect enumeration attacks by monitoring for high-frequency 403/404 responses on resource endpoints.