OWASP API Security Top 10
debt(d7/e7/b5/t7)
Closest to 'only careful code review or runtime testing' (d7). The term's detection_hints.tools is empty. BOLA and authorization failures are logic-level flaws — no static analyzer can reliably detect that a given endpoint fails to verify object ownership. These issues surface only through careful code review (checking every endpoint for ownership assertions) or dedicated authorization-focused penetration/integration testing. They are silent in production until an attacker or tester exercises them cross-user.
Closest to 'cross-cutting refactor across the codebase' (e7). The quick_fix says 'For every API endpoint that returns an object by ID, verify the authenticated user owns or has permission.' The common_mistakes note that it is easy to add an endpoint and forget the ownership check, and that all columns / all endpoints must be audited. Retrofitting authorization checks across every endpoint in a codebase, plus adding automated cross-user tests, plus sunsetting old API versions, is a cross-cutting effort touching many files and potentially the architectural authorization layer.
Closest to 'persistent productivity tax' (b5). The applies_to context is 'web', covering all API-serving PHP applications. Every new endpoint added in the future must include an ownership/authorization check — this is an ongoing discipline tax on every developer adding API features. It doesn't fully define the system's shape (b7) but it meaningfully shapes every API endpoint's design, making it a persistent productivity tax.
Closest to 'serious trap — contradicts how a similar concept works elsewhere' (t7). The canonical misconception is explicit: developers who know the OWASP Web App Top 10 assume authentication coverage equals security coverage, not realizing BOLA is an authorization failure that occurs *after* successful authentication. This contradicts the mental model carried over from general OWASP knowledge and from authentication-focused security training, making it a serious conceptual trap for competent developers.
Also Known As
TL;DR
Explanation
APIs expose different attack surfaces than traditional web applications. OWASP maintains a dedicated API Security Top 10 (2023 edition): API1 — Broken Object Level Authorization (BOLA/IDOR, accessing other users' data by changing an ID); API2 — Broken Authentication; API3 — Broken Object Property Level Authorization (mass assignment, over-posting); API4 — Unrestricted Resource Consumption (missing rate limits); API5 — Broken Function Level Authorization (admin endpoints accessible to regular users); API6 — Unrestricted Access to Sensitive Business Flows (purchasing bots, account enumeration); API7 — Server Side Request Forgery; API8 — Security Misconfiguration; API9 — Improper Inventory Management (shadow APIs, old versions); API10 — Unsafe Consumption of APIs (blindly trusting third-party API responses).
Common Misconception
Why It Matters
Common Mistakes
- Relying on obscurity — using UUIDs instead of sequential IDs does not prevent BOLA; an attacker with one valid UUID can try others from the same entropy space.
- Not testing authorization at every endpoint — it is easy to add an endpoint and forget the ownership check; use automated tests that verify cross-user access is denied.
- Exposing internal model fields in API responses — returning all columns including internal flags, admin notes, or cost prices is API3 (Broken Object Property Level Authorization).
- Not versioning deprecated API endpoints — old versions accumulate security debt and become shadow APIs (API9); always sunset old versions explicitly.
Code Examples
<?php
// ❌ BOLA — no ownership check
// GET /api/orders/{id}
public function show(int $orderId): JsonResponse
{
$order = Order::find($orderId); // Attacker changes ID to another user's order
return response()->json($order); // Returns any order — no auth check
}
<?php
// ✅ Object-level authorization check
public function show(int $orderId): JsonResponse
{
$order = Order::where('id', $orderId)
->where('user_id', auth()->id()) // Only return if owned by current user
->firstOrFail();
return response()->json($order);
}
// ✅ Mass assignment protection (API3)
public function update(Request $request, int $orderId): JsonResponse
{
$order = Order::where('id', $orderId)
->where('user_id', auth()->id())
->firstOrFail();
// Only allow safe fields — never $order->fill($request->all())
$order->update($request->only(['shipping_address', 'notes']));
return response()->json($order);
}