The OWASP API Security Top 10 has not changed since the 2023 edition. The apps behind it have. Single-page frontends talk to a back-end-for-frontend, which talks to a GraphQL gateway, which fans out to a dozen microservices. Mobile clients hit the same APIs directly. New LLM features bolt a chat backend onto endpoints never designed to be called that way. The surface moved, the categories did not, and the same authorization bugs that paid in 2020 still pay now, because they live in business logic that no scanner understands.
This is how I test modern APIs for the categories that actually produce accepted reports: object-level authorization (BOLA), authentication, property-level exposure and mass assignment, function-level authorization, plus the GraphQL and resource-consumption angles. Examples use example.com, placeholder tokens and lab data. The goal is a method a junior tester can reproduce, not a list of endpoints to blindly fuzz.
What changed: BFFs, GraphQL, mobile-first APIs, LLM backends
A few shifts matter for testing:
- BFFs (backends-for-frontend) mean the pretty UI is not where authorization lives. The real checks are one hop deeper, and the BFF often forwards your identity downstream in ways you can probe.
- GraphQL collapses many REST endpoints into one, so one missing check can expose a whole object graph through nested fields.
- Mobile-first APIs are the same APIs with no browser and no same-origin policy in the way — the mobile client is just another HTTP client you fully control.
- LLM backends add endpoints that take free text and call internal functions. They have the same authorization needs as any other, and being new, they are often less reviewed.
None of this invents new bug classes. It moves the old ones to places people forgot to check.
BOLA (API1): the test matrix
BOLA stands for Broken Object-Level Authorization: the API accepts a request that references an object by its identifier and returns or modifies it without checking that this caller is allowed to touch that object. It is number one on the OWASP API list for a reason: it is the single most common serious API bug, and it is invisible to the UI because the UI only ever shows you your own IDs.
The assumption BOLA violates is "the client can only ask for what we showed it". Attackers do not use your client — they change the ID.
The minimal mechanism, horizontal (same privilege, different tenant):
GET /api/v1/invoices/1001 HTTP/1.1
Host: api.example.com
Authorization: Bearer USER_A_TOKEN
GET /api/v1/invoices/1002 HTTP/1.1
Host: api.example.com
Authorization: Bearer USER_A_TOKEN
If 1002 belongs to user B and user A gets it back, that is BOLA. The fix is a server-side check that the object's owner matches the authenticated principal, on every object access, not just at the collection level.
My test matrix, run for every object-bearing endpoint:
- Horizontal: user A requests user B's object. Two accounts, swap IDs.
- Vertical: a low-privilege user requests an object only an admin should see.
- ID formats: sequential integers are easy; UUIDs feel safe but are not authorization. A UUID you can find is a UUID you can request, and IDs leak in other responses, URLs, emails,
Locationheaders, exported files, client-side JS, and older objects you legitimately own. "Unguessable" is not "unauthorised". - Nested objects:
/orders/{id}/items/{itemId}— the parent may be checked while the child is not. - Bulk and batch endpoints:
GET /api/v1/users?ids=1,2,3or a batch body. One missing check leaks many objects at once, and these are often added later without the per-object check the single-object route has.
The misconception to kill: people treat UUIDs as an access control. They are obscurity. Authorization is a check you perform, not a hard-to-guess string.
Broken authentication (API2) in token-heavy apps
Modern APIs lean on bearer tokens, usually JWTs (JSON Web Tokens — signed, self-describing tokens the server validates without a session lookup). The bugs cluster around how the token is validated.
What I check on every JWT:
- Algorithm confusion. Does the server honour the
algheader the client sends? If it acceptsnone, or verifies an RS256 token using the public key as an HMAC secret, the signature is meaningless. The server must pin the expected algorithm, not read it from the token. kidhandling. Thekid(key ID) header picks the verification key. If it is used to build a file path or SQL lookup, it can be abused for traversal or injection. The key set must be fixed server-side.- Expiry. Is
expactually enforced? Replay an expired token and see. - Audience and issuer. A token minted for service X should not validate at service Y — a frequent miss in microservice estates, and it chains well with BOLA across services.
- Refresh flows. Can a refresh token be reused after rotation? Does logout actually invalidate? Is the refresh endpoint rate-limited?
The principle: a token proves who you are (if validated correctly), never what you may touch. Authentication and authorization are different checks; passing the first tells you nothing about the second.
Property-level authorization (API3): over-fetching and mass assignment
API3 is two sides of one coin: the API exposes more than it should on the way out (over-fetching) or accepts more than it should on the way in (mass assignment).
Over-fetching. The UI shows a user's name and avatar. The API response also carries role, isVerified, internalNotes, passwordResetToken — the UI hides them, the API hands them over. Always read the raw response, never the rendered screen.
Mass assignment is the inbound version, and it still pays well. The server binds the whole request body onto the object, including fields the client was never meant to set. The classic:
PATCH /api/v1/users/me HTTP/1.1
Host: api.example.com
Authorization: Bearer USER_TOKEN
Content-Type: application/json
{ "displayName": "Abhi", "role": "admin", "accountId": 42 }
If role or accountId sticks, you have a privilege or tenant escalation. To find the extra fields, diff what the API returns against what the UI uses, then try writing back every field you saw plus obvious guesses (isAdmin, verified, ownerId).
The assumption being violated: "the client only sends the fields in our form". The fix is an explicit allow-list of writable fields per endpoint and role — bind to a DTO you define, never straight onto the database model.
Function-level authorization (API5) and hidden admin routes
BOLA is about objects; BFLA (Broken Function-Level Authorization, API5) is about operations. Can a regular user call an admin function? The method and the route are the test, not the object.
- Change the verb: a user can
GET /api/v1/reports/5— can theyDELETEit? - Call admin routes directly. Admin paths are discoverable in client JS, OpenAPI/Swagger docs, and predictable naming (
/admin,/internal,/v1/management). A hidden button is not an access control. - Watch for routes guarded only in the frontend router. SPA route guards are UX, not security.
The fix is the same shape as BOLA: enforce the role check server-side on every function, deny by default.
GraphQL specifics
GraphQL concentrates several of the above into one endpoint, so I give it dedicated passes.
- Introspection. If the introspection query is enabled in production, it hands you the whole schema — every type, field and mutation, a map of the attack surface. Pull it first if it is on.
- Nested authorization. A query can walk relationships:
me { organization { members { email } } }. The top-level field may be authorized while a nested one is not. Every resolver needs its own check; BOLA hides in the nesting. - Aliases and batching. One request can ask for the same field many times under different aliases, or send many operations at once — multiplying BOLA attempts and defeating naive per-request rate limits.
- Mutations as function-level tests. Each mutation is an operation; run the API5 checks against the ones a low-privilege user should not reach.
Rate limiting and unrestricted resource consumption
API4 (Unrestricted Resource Consumption) is less glamorous but real: endpoints that do expensive work with no ceiling. I check whether a single caller can force heavy queries (deep GraphQL nesting, large page sizes, wide date ranges), whether rate limits are per-user or trivially bypassed by rotating tokens, and whether batching sidesteps limits. On its own it is often low-to-medium; it matters most as an amplifier for the bugs above.
Reporting severity honestly
This is where reports get rejected, and where I see hunters lose credibility. A BOLA that leaks one non-sensitive field is not critical because you called it "full account takeover". Rate the finding on demonstrated impact and the sensitivity of what was actually exposed.
Where severity is earned is in chains. A BOLA that leaks an email, plus a mass-assignment that sets a role, plus a function-level gap on an admin route, can be a genuine account-takeover chain — show it step by step, with requests and responses, rather than inflating any single link. Honest severity gets you taken seriously next time, which is worth more than one padded report.
Checklist
- [ ] Two accounts ready (same level) plus one low-privilege account for vertical tests
- [ ] For every object endpoint: swap IDs horizontally and vertically, including nested and bulk routes
- [ ] Treat UUIDs as data, not access control; hunt where IDs leak
- [ ] Read raw responses for over-fetched fields the UI hides
- [ ] Attempt mass assignment with
role,isAdmin,ownerId,accountId - [ ] Validate JWTs: alg pinning, kid, exp, audience, refresh reuse, logout
- [ ] Test function-level auth: verb changes and direct admin routes
- [ ] GraphQL: introspection, nested resolver auth, aliases/batching, mutations
- [ ] Probe resource limits and whether batching or token rotation bypasses them
- [ ] Rate every finding on demonstrated impact; build chains instead of inflating
Fixes
- Enforce object-level authorization on every access, server-side, comparing object owner to authenticated principal — never rely on ID unguessability.
- Keep authentication and authorization as separate, explicit checks; pin JWT algorithm and key set server-side, and validate
exp, issuer and audience. - Bind inbound bodies to explicit per-role allow-lists (DTOs), never straight onto the data model; strip unknown fields.
- Apply function-level role checks on every operation, deny by default; never treat frontend route guards as security.
- In GraphQL, authorize every resolver, disable production introspection unless intended, and cost-limit depth, aliases and batches.
- Rate-limit per authenticated principal and bound expensive parameters.
Takeaways
The API Top 10 did not change because the hard problems did not: authorization is business logic, and business logic is exactly what automated tools cannot reason about. BOLA, mass assignment and broken function-level authorization keep paying because every new architecture — BFFs, GraphQL, mobile, LLM backends — just relocates the missing check. UUIDs are not authorization. Authentication is not authorization. The UI is not authorization. Test with two accounts and the raw wire, read every field the server returns, try to write every field it accepts, and report what you actually proved.
Learn more
- OWASP API Security Top 10 2023 (owasp.org)
- MITRE CWE-639 (Authorization Bypass Through User-Controlled Key) and CWE-915 (Mass Assignment) (cwe.mitre.org)
- OWASP Web Security Testing Guide — authorization testing (owasp.org)
- OWASP GraphQL Cheat Sheet (owasp.org)
- CVE Program (cve.org)
Related: Framework-Level Bugs: Middleware Bypasses, SSR Fetches and the SSRF Hiding in Your Renderer · Testing AI Agents and MCP Servers. Need this tested on your product? Book a free 30-minute scoping call.



