OWASP API Top 10 (2019) — Drill Notes
OWASP API Top 10 (2019) — Drill Notes
Section titled “OWASP API Top 10 (2019) — Drill Notes”Walking the 2019 list category by category. Each entry: what it is, how it differs from neighbours, how you test it.
API1: Broken Object Level Authorization (BOLA)
Section titled “API1: Broken Object Level Authorization (BOLA)”Essentially IDOR reframed for APIs. Same root cause — the server authenticates you, then fails to check that this object is yours.
GET /api/users/123 ← your own ID (from your profile)GET /api/users/124 ← swap it → someone else's record returnedIDOR vs BOLA
Section titled “IDOR vs BOLA”| IDOR (web era) | BOLA (API) | |
|---|---|---|
| Framing | web app, page/param reference | API endpoint + resource ID |
| “Direct” reference assumed? | yes — guessable IDs, filenames, paths | no |
| Where it lives | A4:2013 → A01:2021 Broken Access Control | API1 2019, still API1 2023 |
| Scope | any unauthorized object access | specifically missing object-level authorization |
The critical difference: IDOR implied guessable references, so “use UUIDs” became folk advice. BOLA explicitly covers unguessable IDs you obtained legitimately — a listing endpoint returns objects you shouldn’t see, you take an ID from it, then swap. Random IDs are defense in depth, not a fix.
Why it’s #1 for APIs
Section titled “Why it’s #1 for APIs”- APIs expose IDs as a first-class interface — that’s the design
- Multiple clients (web, mobile, third-party) → inconsistent enforcement across routes that “should” be equivalent
- No UI hiding the IDs; JSON hands you raw values
- Clients cache objects and replay them
- Trivially automatable: iterate IDs, harvest
BOLA vs BFLA — don’t conflate
Section titled “BOLA vs BFLA — don’t conflate”| BOLA (API1) | BFLA (API5) | |
|---|---|---|
| Level | object — this record | function — this endpoint/action |
| Example | GET /users/124 as user 123 |
DELETE /users/124 as a normal user |
| Question | “may I touch this object?” | “may I perform this action at all?” |
BOLA = /api/orders/{id} with someone else's idBFLA = /api/admin/users called with a non-admin tokenTest recipe
Section titled “Test recipe”1. Log in as a low-privilege user; capture every request2. Enumerate IDs — sequential integers, and UUIDs harvested from list endpoints3. Swap for another user's object → 200 with their data = BOLA4. Repeat across methods (GET/PUT/PATCH/DELETE) — authorization is often enforced on read and forgotten on write5. Test horizontally (peer user) AND vertically (other tenant/org)Step 4 is why write methods matter: read-only checks are common, write checks are skipped.
✗ never authorize from the client-supplied ID✓ derive ownership from the session/token subject, then filter by it✓ one centralized per-object check — not copy-pasted per route✓ enforce across every method and every route variant (/v1, /v2, legacy)API2: Broken User Authentication
Section titled “API2: Broken User Authentication”The one category where the user is the attack surface. Everything else targets objects, functions, or data; API2 targets the act of proving identity.
Scope — what’s in, what’s out
Section titled “Scope — what’s in, what’s out”IN who are you? credentials, tokens, sessions, reset flowsOUT what may you do? → that's BOLA (API1) and BFLA (API5)API2 is authN failing. BOLA, BFLA, and property-level are authZ failing. See the authN vs authZ page.
Attack surface by mechanism
Section titled “Attack surface by mechanism”| Mechanism | How it breaks |
|---|---|
| Password login | credential stuffing, brute force, no lockout, weak policy, timing / user enumeration |
| Password reset | guessable token, token in URL → Referer + logs, no expiry, not single-use, host-header injection |
| API keys | in URL query (logged by proxies), leaked in JS/mobile/git, no expiry, no rotation, one key shared by all users |
| Session | predictable IDs, no rotation on login, no invalidation on logout, missing HttpOnly/Secure/SameSite |
| JWT | alg:none, weak HMAC secret, kid injection, unchecked exp, no revocation |
| Basic Auth | base64 assumed encrypted, password resent every request, no logout story |
| OAuth | redirect_uri manipulation, missing state, implicit-flow leakage |
| MFA | absent, bypassable by response manipulation, or enforced on only one endpoint |
1. Credential stuffing vs brute force
Section titled “1. Credential stuffing vs brute force”Brute force = many passwords against ONE accountCredential stuff = ONE known password against MANY accounts (leaked lists)Stuffing is what kills real APIs. It is low-noise (each attempt hits a different account, so per-account lockout never fires), high-yield (password reuse), and distributed.
Per-account lockout does not stop stuffing. You need per-IP rate limits, device fingerprinting, or breach-list checks.
2. Password reset — the most-missed surface
Section titled “2. Password reset — the most-missed surface”✗ token = user_id, a timestamp, or an incrementing value✗ token sent in the URL → lands in Referer, proxy logs, browser history✗ token reusable and/or never expires✗ host-header injection: the reset link is built from the Host header → attacker sets Host to their own domain and harvests the token✗ user enumeration: different response for "email not found" vs "sent"Enumeration is the entry point for stuffing — harvest valid accounts from reset/login responses first, then stuff.
3. Token lifetime and invalidation
Section titled “3. Token lifetime and invalidation”✗ no expiry✗ logout only clears the client; the server-side token stays valid✗ password change does not invalidate existing tokens✗ refresh token with no rotation and no revocationTest: log in, capture the token, log out, replay the token. Works = API2 finding. Then change the password and replay again.
4. JWT
Section titled “4. JWT”Every JWT failure is an API2 finding — full detail on the JWT page:
alg:none → forge any identityweak HMAC secret → offline brute force (hashcat -m 16500)kid injection → path traversal / SQLi into the key lookupexp unchecked → token valid foreveraud / iss unchecked → a token minted for ANOTHER app is acceptedThat last one is the subtle killer: a token issued for a different service in the same ecosystem gets accepted here.
Test recipe
Section titled “Test recipe”1. Enumerate users → reset/login responses that differ for known vs unknown2. Rate limit → 100 rapid logins, watch for 429 or lockout3. Stuffing → many accounts, ONE common password, distributed source4. Reset flow → request a reset; is the token guessable? in the URL? single-use? expiring?5. Token replay → log out, replay the token; change password, replay again6. Token in transit → any token in a URL, query string, or Referer?7. JWT → decode; test alg:none; brute the secret; check exp/aud/iss8. Method bypass → /login is rate-limited but /api/v1/login is notStep 8 recurs constantly — protection applied to the current route and forgotten on legacy variants (ties into API9 Improper Assets Management).
✓ centralize authentication — one implementation, not per-route copies✓ rate limit per IP AND per account, on reset/OTP too, not just login✓ MFA on sensitive actions✓ tokens: short-lived, rotated, revoked on logout AND on password change✓ reset tokens: random 128-bit+, single-use, short expiry, in the BODY not the URL✓ never expose tokens in URLs✓ verify JWTs: allow-listed alg, checked exp/aud/iss, long secret✓ constant-time comparison; identical responses for unknown user vs wrong passwordThe API2 / API4 overlap
Section titled “The API2 / API4 overlap”Auth-specific rate limiting genuinely sits in both:
API2 (2019) → "no rate limiting on auth endpoints" is listed as an auth failureAPI4 (2019) → "Lack of Resources & Rate Limiting" is its own categoryExam distinction:
API2 → limiting on AUTH operations specifically (login, OTP, reset)API4 → limiting on ANY resource-consuming operationBrute-forcing /login with an authentication framing → API2. DoS or enumeration through a general endpoint → API4.
2023 status: API2 stays “Broken Authentication” at #2 — one of the three (with BOLA and BFLA) that survive both editions because they are architectural, not patchable implementation bugs.
API3: Excessive Data Exposure
Section titled “API3: Excessive Data Exposure”The API hands over more than the client needs and trusts the UI to hide the rest.
The core fallacy
Section titled “The core fallacy”Developer: "the app only renders name and avatar"Reality: the JSON body contains everythingAttacker: reads the raw response — never touches the UIClient-side filtering is not security. Whatever is transmitted has already leaked.
Where it comes from
Section titled “Where it comes from”| Cause | What happens |
|---|---|
| Serializing the ORM/DB model directly | every column ships, needed or not |
| “Just return the object” | no response schema, no DTO |
| Backwards compatibility | old clients consume old fields, so fields are never removed |
| Shared endpoint across clients | web needs more than mobile → mobile receives web’s payload |
| Verbose/debug mode left on | stack traces, config, internal IDs |
| Generic list/detail endpoints | detail returns full records, list returns nearly as much |
What actually leaks
Section titled “What actually leaks”password hashes, reset tokens, session IDs, API keysisAdmin, role, permissions, internal flagsinternal notes / admin comments about the usersoft-delete flags, internal IDs, DB timestampsother users' PII inside nested objectspricing / cost / margin fieldsGPS coordinates, IP addresses, device fingerprintsThe nested case is the sneaky one: /api/orders legitimately returns your order, but each order embeds a full user object carrying fields you should never see.
API3 vs its neighbours
Section titled “API3 vs its neighbours”| Question | Example | |
|---|---|---|
| BOLA (API1) | right fields, wrong object? | GET /users/124 as user 123 |
| API3 (2019) | right object, too many fields? | GET /users/123 returns password_hash, ssn, isAdmin |
| Mass Assignment (API6) | can I write fields I shouldn’t? | PATCH /users/me {"role":"admin"} |
BOLA = object-level authz missingAPI3 = property-level authz missing (READ side)Mass Assignment = property-level authz missing (WRITE side)That is why 2023 merged them: Excessive Data Exposure + Mass Assignment → API3 Broken Object Property Level Authorization (BOPLA). Read and write are the same defect viewed from two sides.
Test recipe
Section titled “Test recipe”1. Ignore the UI entirely — read raw JSON bodies2. Diff: what the screen renders vs what the response contains3. Hunt for the field names listed above4. Compare the DETAIL endpoint against the LIST endpoint — detail is usually fatter5. Check nested objects: does an embedded user/org object carry extras?6. Compare across clients — mobile API vs web API for the same resource7. Force errors and read the verbose response8. Send extra params (?include=all, ?verbose=1, ?fields=*)Step 4 matters — teams often tighten the list serializer and forget the detail one.
The GraphQL variant
Section titled “The GraphQL variant”Same defect, different mechanism:
- introspection exposes the whole schema (types + fields unreachable via REST)- "did you mean" suggestions leak field names for typos- request fields you can see but should not read- nested traversal: user → orders → payments → other usersFix: disable introspection in production and authorize at the field level, not just the query level.
✓ explicit response schemas / DTOs — serialize on an allow-list, never the raw model✓ never bind the ORM entity straight to the response✓ a public "view" object distinct from the internal entity✓ strip sensitive fields centrally at the serializer layer✓ authorize per-field where data is role-dependent✓ disable verbose errors in production✓ review nested serializers — they leak by aggregationThe rule: the output contract must be a deliberate, minimal, named shape — not “whatever this object happens to contain today.”
Why it persists
Section titled “Why it persists”It is invisible in testing. The UI looks correct, there is no error and no anomaly — the extra data sits silently in the response body. Unless someone reads the raw JSON, nothing signals a problem.
API4: Lack of Resources & Rate Limiting
Section titled “API4: Lack of Resources & Rate Limiting”The full 2019 name includes Resources — that half matters, and it is what 2023 expanded into.
Two things bundled, one defect
Section titled “Two things bundled, one defect”Rate limiting = too many requests over time (the time dimension)Resources = one request too expensive (the size / depth dimension)Same root cause: no accounting of per-request cost. A single 100MB upload can hurt more than 10,000 small calls, so the category covers both.
What’s missing — the checklist
Section titled “What’s missing — the checklist”| Dimension | Missing control |
|---|---|
| Request count | no throttling on login, OTP, reset, search, export |
| Payload size | no cap on upload or JSON body |
| Response size | pagination with no maximum page size |
| Execution time | no timeout — ?sleep=3600, slow queries |
| Query depth | unbounded GraphQL nesting, expensive filters |
| Upstream fan-out | one request triggers 10+ backend calls |
| Storage | unlimited uploads, no quota |
The attacks
Section titled “The attacks”1. Brute force / stuffing → ties into API22. Enumeration → harvest IDs, emails at scale3. Flood / DoS → raw volume4. COST AMPLIFICATION → economic DoS5. Resource exhaustion → giant image, decompression bomb, ReDoS6. Unbounded pagination → ?limit=10000007. GraphQL complexity bomb → nested query explodes exponentially8. Long-running operations → expensive filters, report generationCost amplification is the interesting one:
attacker sends: 1 cheap requestserver does: S3 scan + 3 upstream API calls + ML inference + full table scanresult: attacker pays ~$0, you pay per-invocation foreverThat is why 2023 renamed the category — it is not just “too many requests”, it is per-request economics.
ReDoS belongs here too: a catastrophic regex like (a+)+$ lets one tiny request consume a CPU core. Shield is blind to it — it is L7 application code, not network flood.
Rate-limit bypass — the pentesting meat
Section titled “Rate-limit bypass — the pentesting meat”Finding a limit is not enough. Test whether it can be evaded:
IP-based → spoof the header the limiter trusts: X-Forwarded-For: <random> X-Real-IP, X-Client-IP, True-Client-IP, Forwarded
Path-based → vary the path, same handler: /api/login vs /API/login (case) /api/login vs /api/login/ (trailing slash) /api//login (double slash) /api/login%2f (encoded) /api/login?a=1 (random query param)
Route variants → /v1/login vs /v2/login vs /api/v2/login (ties to API9)Method override → X-HTTP-Method-Override: POSTIdentity rotation → multiple API keys, multiple accountsProtocol trick → IPv6 vs IPv4 when keyed on addressDistribution → rotating proxiesPath variation is the most common real finding — the limit is keyed on the URL string while the router normalizes to the same handler. Legacy-route bypass (/v1 unthrottled, /v2 throttled) is the same class as API2 step 8.
Detecting whether a limit exists
Section titled “Detecting whether a limit exists”- send 100+ rapid requests, watch for 429 or 503- read headers: X-RateLimit-Limit, X-RateLimit-Remaining, Retry-After- probe the boundary: after a 429, does the counter reset on a fixed window or a sliding one? (fixed window = burst allowed at the boundary edge)- distinguish lockout from throttle (account lock is a different control)- check whether the limit is per-IP or per-account — then rotate the other oneThe API2 / API4 distinction
Section titled “The API2 / API4 distinction”API2 → limiting on AUTH operations (login, OTP, reset)API4 → limiting on ANY resource-consuming operationBrute-forcing /login framed as authentication → API2. Flooding a search or export endpoint → API4.
✓ limit per IDENTITY (user + API key + IP), not just globally✓ limit at multiple layers — gateway AND application (gateway alone is bypassable with the path variants above; application alone is bypassable with distributed load)✓ token bucket or sliding window, not naive fixed counters✓ separate limits for expensive operations✓ cap payload size, page size, query depth, execution time✓ timeouts on every upstream call✓ GraphQL: depth + complexity limits✓ cost-weighted quotas for third-party-billed operations2019 → 2023
Section titled “2019 → 2023”2019: Lack of Resources & Rate Limiting2023: Unrestricted Resource ConsumptionWhy renamed: broadened from “rate” to all resource cost — CPU, memory, bandwidth, storage, and third-party spend. The framing shifted from “you may not send too many requests” to “a single request may not cost too much.” The 2023 edition explicitly calls out third-party paid services, which is the cost-amplification angle.
API5: Broken Function Level Authorization (BFLA)
Section titled “API5: Broken Function Level Authorization (BFLA)”Reach administrative or privileged functions as a normal user. See the BOLA/BFLA table above.
- call /admin/* endpoints with a regular token- swap the HTTP method (GET allowed, DELETE not checked)- guess alternate paths (/users vs /admin/users vs /internal/users)- use the admin route from a different API version still deployedAPI6: Mass Assignment
Section titled “API6: Mass Assignment”Bind client-supplied JSON straight into the model — attacker sets fields the API never intended to expose.
PATCH /api/users/me{ "name": "Fitra", "role": "admin", "balance": 999999, "email_verified": true } ↑ these get written if the object is bound without an allow-listTest: add fields to every write request — role, isAdmin, verified, balance, ownerId. Watch the response and a subsequent GET.
API7: Security Misconfiguration
Section titled “API7: Security Misconfiguration”- default credentials on admin/logging interfaces- CORS: Access-Control-Allow-Origin: * with credentials- verbose errors / stack traces in responses- missing security headers- unpatched or debug endpoints left enabled- TLS misconfig, cloud storage left publicAPI8: Injection
Section titled “API8: Injection”SQLi, NoSQLi, command injection, LDAP — classic injection reaching through API inputs.
{"email": {"$ne": null}} ← NoSQL operator injectionemail=a' OR '1'='1 ← SQLi in a JSON string fieldAlso worth noting: XXE on SOAP/XML endpoints — see the REST vs SOAP page.
API9: Improper Assets Management
Section titled “API9: Improper Assets Management”Old and undocumented versions still live and less protected.
/api/v1/... ← still deployed, still accepts the old token, no rate limit/api/v2/... ← current, hardened→ attacker uses v1Also: staging/debug hosts, .env, .git/config, /phpinfo.php reachable. (Exactly the paths you saw being fuzzed through Caido.)
Test: probe version paths, IP-literal hosts, and non-production subdomains.
API10: Insufficient Logging & Monitoring
Section titled “API10: Insufficient Logging & Monitoring”Attacks are invisible — no logging, no alerting, no detection path.
- enumeration and brute force go unrecorded- no alert on anomalous access patterns- logs lack the identity that made the requestPrimarily a defensive finding, but it’s what lets every other category go unnoticed.
2019 → 2023 movement
Section titled “2019 → 2023 movement”| 2019 | 2023 | Change |
|---|---|---|
| API3 Excessive Data Exposure | merged into API3 Broken Object Property Level Authorization | 2019 split read-exposure from writable-fields; 2023 fuses both into property-level authz |
| API6 Mass Assignment | merged into API3 | mass assignment is one form of property-level write abuse |
| API4 Lack of Resources & Rate Limiting | → Unrestricted Resource Consumption | broadened from rate limits to all resource cost (payload size, CPU, third-party spend) |
| API8 Injection | removed | generic web risk, not API-specific |
| API10 Insufficient Logging & Monitoring | removed | generic observability, not API-specific |
| — | NEW: API6 Unrestricted Access to Sensitive Business Flows | business-logic abuse (stock buyout, coupon farming, scalping) |
| — | NEW: API7 SSRF | API fetches a user-supplied URL → internal services, cloud metadata |
| — | NEW: API10 Unsafe Consumption of APIs | blindly trusting third-party API responses |
What survives both versions: BOLA, Broken Auth, BFLA. They persist because they are architectural — not implementation bugs you can patch away. If you learn only three, learn those.
Sources
Section titled “Sources”- OWASP API Security Top 10 — 2019 and 2023 editions
- CAPIJ/CAPIE course: chapter 3 (OWASP 2019 articles) and chapter 4 (2023 practical demos)