OWASP API Security Top 10
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);
}