API Security and the OWASP API Top 10
On this page
- The Attack Surface Raised to the Power of the Web
- BOLA: The Number-One API Flaw
- BFLA: Function-Level Authorization
- Broken Authentication and Excessive Data Exposure
- Injection, Asset Management, and Consumption
- CORS Is Not an Access Control
- The Defence Layers
- The Review Practice That Catches Most of It
- Checklist
The Attack Surface Raised to the Power of the Web#
As applications moved from server-rendered HTML to JSON APIs consumed by single-page apps and mobile clients, the attackers followed. APIs multiply the impact of every classic web flaw: one endpoint missing an authorization check now returns thousands of records in a single call, and every microservice is a new reachable surface, often exposed directly to the internet with no browser in front of it to enforce anything. The OWASP API Security Top 10 is the standard map for this surface, and it differs meaningfully from the web Top 10 because APIs fail in their own ways.
BOLA: The Number-One API Flaw#
Broken Object Level Authorization (BOLA) is the API name for IDOR, and it tops the list because APIs hand out object references by design. GET /api/orders/1942 that returns the order without checking ownership is the whole flaw. The fix is the same: scope every object lookup to the authenticated caller, return identical responses for missing and forbidden objects, and apply the check to batch parameters and every node in a GraphQL response, not just the top-level request.
# The one control that matters, applied on every object endpoint
def get_order(order_id, user):
order = db.orders.find_one({"_id": order_id, "owner_id": user.id})
return order or abort(404)
BFLA: Function-Level Authorization#
Broken Function Level Authorization sits on top of BOLA: administrative or privileged endpoints reachable by any authenticated client. A regular user calling POST /api/admin/promote or DELETE /api/users/5 succeeds because the server checks that the caller is logged in but not that their role permits the function. APIs make this worse because there is no UI hiding the button, the routes are simply there to be called, discoverable by reading the client's JavaScript or guessing conventions. Enforce role checks centrally and test every privileged route from a low-privilege account.
Broken Authentication and Excessive Data Exposure#
APIs reinvent authentication, often badly: API keys sent in query strings (which leak into server logs, proxies and browser history), tokens that never expire or are never revoked, weak password-reset flows, and missing rate limits that enable credential stuffing. Use short-lived, scoped tokens in the Authorization header, never in the URL; the auth concepts lesson covers token lifecycle.
Excessive Data Exposure is a distinctively API problem. The endpoint returns the full database record and relies on the client to display only some fields, so the sensitive ones (password hash, internal notes, another user's email, an is_admin flag) are shipped over the wire and simply not rendered. Anyone inspecting the raw response sees everything. The fix is to serialize an explicit response schema (a DTO) per endpoint that lists exactly the fields the client needs, rather than dumping the model:
# Instead of returning the whole user row, return only what the client needs
def public_user(user) -> dict:
return {"id": user.id, "name": user.name, "avatar_url": user.avatar_url}
# password_hash, email, role, internal_notes are never serialized
The mirror-image flaw is mass assignment, where an endpoint binds the whole incoming JSON body onto the model, letting a client set is_admin=true or owner_id=1. Bind only an allowlist of fields per action.
Injection, Asset Management, and Consumption#
- Injection applies to APIs exactly as elsewhere: SQL and command injection via JSON values. NoSQL databases add operator injection, where a JSON body like
{"password": {"$ne": ""}}can bypass a login that builds a query straight from the request. Validate types strictly (a password must be a string, not an object) and use parameterized queries. - Improper Asset Management is the API version of "we forgot that was public": old
/v1/endpoints left running after/v2/shipped, staging and debug hosts reachable from the internet, and undocumented endpoints. You cannot secure what you do not know exists, so maintain an inventory of every exposed host and route (supply chain security covers inventory discipline). This is frequently the finding that opens a whole assessment. - Unrestricted Resource Consumption: without rate limits and payload caps, an API is open to brute force, scraping, and denial of service. A single expensive GraphQL query or an unbounded
?limit=can exhaust the backend. Enforce rate limits, pagination caps, query cost limits, and request size limits.
CORS Is Not an Access Control#
A recurring confusion is treating CORS (Cross-Origin Resource Sharing) as a security boundary for an API. It is not. CORS is a browser mechanism that decides whether browser JavaScript from another origin may read your API's responses; it does nothing to stop a non-browser client (curl, a script, a mobile app, an attacker's server) from calling the API directly. A permissive Access-Control-Allow-Origin: * combined with credentials is a real misconfiguration, but a restrictive CORS policy is not authentication and not authorization. Your API must authenticate and authorize every request on the server regardless of CORS headers, because CORS only governs one specific client type. Teams that "locked down CORS" and assumed the API was protected have shipped wide-open endpoints to every non-browser caller.
The Defence Layers#
Tie the controls together into a coherent posture:
| Layer | Control |
|---|---|
| Gateway / middleware | Authentication, rate limiting, allowlisted methods, request size caps |
| Per-route validation | Schema that rejects unknown fields (kills mass assignment) and wrong types |
| Authorization | Object-level ownership checks and role gates, applied centrally |
| Serialization | Explicit response DTOs, never the raw model |
| Transport | TLS everywhere; mTLS for machine-to-machine where the client must prove identity |
| Observability | Structured logging of who called what, feeding detection and incident response |
For sensitive or internal APIs, prefer short-lived scoped tokens over long-lived master keys, and consider mutual TLS (mTLS) for service-to-service traffic, where each client presents its own certificate so the server authenticates who is calling, not just that they hold a shared secret.
The Review Practice That Catches Most of It#
The single most effective habit is to build an authorization matrix for each endpoint: who may call it, in which role, against which objects. Then run the API as the wrong user and confirm the denial. Automate that matrix in CI as contract tests that assert both the exact response shape and the status code per role. The value is that the moment an endpoint starts echoing a new column (excessive data exposure) or an authorization check is dropped in a refactor (BOLA/BFLA), a passing test suite is the difference between a quiet drift into exposure and a red build that stops it before release.
Checklist#
- Every object endpoint scopes access to the caller; missing and forbidden return the same status.
- Privileged endpoints enforce role checks and are tested from low-privilege accounts.
- Responses are built from explicit DTOs; no raw model is serialized.
- Request bodies bind only allowlisted fields; inputs are type-validated (guarding NoSQL operator injection).
- Tokens are short-lived and header-borne, never in URLs; rate limits and payload caps are enforced.
- An inventory of exposed hosts and versions exists; old and staging endpoints are retired.
- An authorization matrix is encoded as CI contract tests across roles.
Check your understanding
4 quick questions on this lesson. Get them all right and the lesson is marked complete.