API security is the bulk of web application security in 2026, because most "web apps" are now a thin client talking to a wide fleet of REST, GraphQL, and gRPC endpoints. The classical browser-page attack surface shrank as frameworks closed the easy injection and XSS cases; the API surface grew to replace it, and grew faster than the defences caught up. The bugs are different in shape from the page-rendering bugs: object IDs in URLs that nobody authorised, JSON bodies with extra fields that get bound straight onto database rows, JWTs signed with the wrong algorithm, GraphQL queries that pull the whole graph in one request.
This article is the spoke that sits next to the web application security hub. I walk the OWASP API Security Top 10 (2023 edition) end to end, go deep on BOLA (the bug that has held the top slot for years), cover the JWT-specific and GraphQL-specific and REST-specific attack patterns, run through the four canonical real-world breaches, and finish with the defences worth investing in. For the tooling side, the best API security tools for 2026 listicle is the companion.
In short: why API security is a bigger problem than web-app security
An API is a contract that returns data to anyone who can speak the protocol, and the bug pattern is almost always the same: the server trusts a field in the request that the caller controls. The classical web app rendered a page for a logged-in user and the server-side template decided what to include; an attacker had to coax the page-rendering layer into leaking. A modern API endpoint returns a JSON object the client asked for by ID, and the server very often forgets to check that the caller is allowed to see that ID. The surface is larger because there are now hundreds of endpoints per product instead of dozens of pages, each endpoint accepts a structured body with many fields instead of a small form, internal services expose their own APIs that the public app eventually proxies through to, and inventories of which API versions are still live tend to drift faster than anyone documents them. Authentication is necessary but rarely sufficient: most API breaches in the last five years involved a logged-in caller asking for somebody else's data through a perfectly authenticated request.
Why the API surface is now larger than the web-app surface it replaced
The shift to API-first products was almost complete by 2020 and has been the dominant pattern since. The browser app is a React or Vue client that does nothing but call the backend; the mobile app does the same; the partner integration does the same; the internal admin tool does the same. Every one of those clients hits the same fleet of endpoints, and each new client tends to surface a new field, a new query parameter, or a new bulk operation that the backend then has to implement. The number of endpoints per product I have audited has gone from a few dozen in 2018 to several hundred in 2026.
Two structural reasons the surface is harder to defend than the old page surface:
- Endpoints accept structured input. A page form had a fixed set of named fields and a CSRF token; an API endpoint accepts a JSON body with arbitrary keys, an arbitrary nesting depth, and a content type the caller chose. Every extra field is a potential mass-assignment target, every nested object is a potential SSRF source, every alternative content type is a parser the developer did not test.
- Object IDs are first-class in the URL. A page rendered "your profile" from the session; an API endpoint renders
/users/{id}/profilefrom a path parameter. The session tells you who the caller is; the path parameter tells you what they want. Wiring those two together correctly is the entire job of authorisation, and it is the job the OWASP API Top 10 has documented the industry getting wrong, year after year.
OWASP API Security Top 10 (2023 edition)
The 2023 list is the current version. The 2019 list was the first edition; 2023 is a clean revision that promoted Broken Object Property Level Authorization to its own slot, added Unrestricted Access to Sensitive Business Flows, and merged Improper Assets Management into Improper Inventory Management. The full reference lives at the OWASP API Security Top 10 (2023) index; the short version follows.
API1:2023 Broken Object Level Authorization (BOLA)
The caller is authenticated, asks for an object by ID, and the server returns it without checking that the caller is allowed to see that object. This has held the number-one slot in every edition of the list because the bug is structural: every endpoint that takes an object ID has to perform the check, and a single endpoint that forgets is a full data-extraction primitive. Detail below in the dedicated BOLA section.
API2:2023 Broken Authentication
The caller's identity claim is accepted on weaker evidence than it should be. The category covers everything from missing rate limits on login (credential stuffing), to password-reset flows that leak the token in a Referer header, to JWT acceptance of alg=none, to weak session-token entropy, to bearer tokens that never expire. The Peloton 2021 incident is the textbook case: API endpoints that returned user data were callable without any authentication header at all.
API3:2023 Broken Object Property Level Authorization (BOPLA, mass assignment)
The caller is allowed to read or write some properties of an object but not all of them. The server binds the request body to the object without filtering, so the caller can set or read fields outside their authorisation. The classical example is a PATCH /users/me that accepts {"is_admin": true} and binds it onto the user row because the framework's auto-binding does not know which fields are sensitive. The 2019 list called this two separate things (Excessive Data Exposure and Mass Assignment); the 2023 list merges them.
API4:2023 Unrestricted Resource Consumption
The endpoint accepts inputs that let the caller make the server do unbounded work: list endpoints with no page size cap, search endpoints with no query-complexity cap, file-upload endpoints with no size cap, account-creation endpoints with no rate cap. The result is either a cost-amplification attack (the attacker forces the operator to pay for compute and bandwidth) or a denial of service.
API5:2023 Broken Function Level Authorization (BFLA)
The caller can invoke administrative or privileged operations that should be limited to a different role. BOLA is "you accessed an object you should not have"; BFLA is "you invoked a function you should not have". The bug is usually that the admin API is on the same host with no per-route role check, and the caller just guesses the path (/api/admin/users, /api/v2/admin/...).
API6:2023 Unrestricted Access to Sensitive Business Flows
New in 2023. The endpoint is correctly authenticated and authorised per request, but the underlying business flow assumes humans use it at human speed. Automation breaks the assumption: ticket-buying scripts that buy out an event, product-listing scrapers that exfiltrate the catalogue, signup flows that mint thousands of accounts, refer-a-friend flows that drain the credit pool. The category exists because traditional rate limiting per IP is not the right defence here; the defence is at the flow layer (device fingerprint, behavioural signals, queue-based throttling).
API7:2023 Server-Side Request Forgery (SSRF)
The endpoint accepts a URL from the caller and the server makes a request to it. Without strict validation, the caller redirects the server to internal resources: cloud metadata services (169.254.169.254), internal admin panels, internal databases. Full treatment at server-side request forgery.
API8:2023 Security Misconfiguration
The wide bucket: missing security headers, verbose error responses that leak stack traces, debug endpoints left enabled in production (/actuator/*, /_debug/*, /.env), CORS configured as * with credentials, default credentials on admin interfaces, TLS misconfiguration, container images shipped with build-time secrets. Every audit finds at least one of these; most find half a dozen.
API9:2023 Improper Inventory Management
The team does not know what APIs they expose. Old versions are still live (/api/v1 is deprecated but still serving), staging endpoints are reachable from production DNS, third-party integrations published an API the security team has not reviewed, internal services are exposed at the edge by misconfigured ingress rules. The fix is documentation discipline plus discovery scanning; the bug is usually the gap between the two.
API10:2023 Unsafe Consumption of 3rd-Party APIs
The flip side: your service trusts data coming back from a third-party API more than it should. The third party returns a redirect URL that points at your internal network (SSRF by proxy), a payload with HTML that you render, a response with size you did not bound. Treat third-party responses with the same suspicion as user input.
BOLA in depth: the #1 API bug for years
Broken Object Level Authorization is the one bug class to internalise if you only have time for one. It is the IDOR (insecure direct object reference) pattern adapted for APIs, and it is everywhere because the structural mistake is small enough to slip past every code review that does not have authorisation as a line item.
The textbook vulnerable handler, in pseudocode:
@app.get("/api/v1/invoices/{invoice_id}")
def get_invoice(invoice_id: int, user = Depends(current_user)):
return db.invoices.find_one(id=invoice_id)The function is "authenticated" because current_user is wired up. It is not authorised: the database query never references user.id. Any logged-in caller can request any invoice by ID. Iterating through /api/v1/invoices/1, /api/v1/invoices/2, ... extracts every invoice on the platform.
The fix is to include the caller in the query:
@app.get("/api/v1/invoices/{invoice_id}")
def get_invoice(invoice_id: int, user = Depends(current_user)):
invoice = db.invoices.find_one(id=invoice_id, owner_id=user.id)
if invoice is None:
raise HTTPException(404)
return invoiceThat is the entire defence at the handler layer. The reason BOLA still ranks number one is that every endpoint has to do this, every new endpoint added by every engineer has to remember to do this, and there is no framework default that does it for you.
BOLA in REST APIs
REST endpoints expose object IDs in the URL: /orders/4711, /projects/abc-123/files/xyz. The pattern of guessing IDs and walking the integer space is the canonical attack. Sequential integer IDs make the walk free; UUIDs slow it down but do not stop it, because the attacker often has at least one valid ID (theirs) and can find more through leaks elsewhere (shared links, support tickets, OAuth-state parameters, image filenames). Treat opaque IDs as a small speed bump, not a defence.
BOLA in GraphQL via node IDs
GraphQL exposes objects through node IDs (the Relay convention) or through typed fields with their own IDs:
query { node(id: "VXNlcjo0NzEx") { ... on User { email phone } } }
The base64-encoded ID decodes to User:4711. The pattern is the same: enumerate the ID space, query each node, exfiltrate. The GraphQL surface is worse than REST here because a single endpoint serves the entire graph; one missing authorisation check on one resolver leaks every object reachable through it.
BOLA via JWT subject claims
A subtler variant: the endpoint uses the JWT's sub claim as the object identifier, and the JWT was issued in a context where the sub is attacker-controllable. A common shape is an account-linking flow that mints a JWT with sub: provider-user-id, where the provider user ID is whatever the OAuth provider returned, and the resource endpoint trusts the sub as the user identifier without re-binding it to the local account. The attacker registers an account with a chosen provider ID, gets a JWT, and reaches into the resource that the local account never owned. The fix is the same shape as above: never trust the request-shaped identifier; resolve to a server-side account record and bind every query to that record.
Mass assignment exploitation
Mass assignment is the bug you get when the server-side ORM happily writes whatever fields the request body contains, and the developer assumed the client would only send fields they were allowed to set. The classical 2012 GitHub incident (Egor Homakov added himself to the Rails core team by adding a hidden form field to a public key creation request that set the public-key owner) is the canonical example. Every framework has a flavour of it.
The vulnerable Rails snippet:
def update
@user = User.find(params[:id])
@user.update(params[:user])
end
params[:user] is whatever the client sent. If they sent { "is_admin": true, "email": "x@y.com" }, both columns are written. Rails added strong_parameters to force an explicit allow-list, but the original bug pattern survives in every framework that auto-binds request bodies onto models without filtering: Spring's @RequestBody onto a JPA entity, Django REST Framework's ModelSerializer with default fields, Express handlers that spread the body into a Mongoose update.
The fix is allow-list at the boundary, every time:
@user.update(params.require(:user).permit(:email, :name))
Two practical notes. First, the bug is bidirectional: mass assignment on writes is the textbook case, but the same shape causes excessive data exposure on reads (the serialiser dumps every column including the password hash, and the client decides what to display). Always define a per-endpoint serialiser, never let the ORM choose. Second, the bug compounds with BOLA: if update lets the caller set owner_id, they can reassign objects to themselves and read them on the next request without any other vulnerability.
JWT-specific attacks
JWTs are bearer tokens shaped like header.payload.signature. The signature is the security boundary; the header tells the verifier which algorithm to use. Every JWT bug I have seen falls under "the verifier did not actually verify". The current best practices live in RFC 8725, JSON Web Token Best Current Practices.
alg=none
The original sin. JWT defined none as a valid algorithm value meaning "no signature". A naive verifier that picks the algorithm from the token header itself will happily accept a token with {"alg":"none"} and an empty signature. The attacker takes any valid token, edits the payload to make themselves an admin, sets alg to none, drops the signature, and the server accepts it. CVE-2018-1000531 is one of many examples (jjwt accepting alg=none in unauthenticated paths). The fix is to never let the token tell the verifier which algorithm to use; pin the expected algorithm on the server side.
kid path traversal and SQL injection
The kid (key ID) header tells the verifier which key to use to verify the token. Implementations that load the key from disk by treating kid as a path are vulnerable to traversal (kid: "../../../../dev/null" makes the verifier load /dev/null as the key and verify the HS256 signature against the empty file). Implementations that load the key from a database by interpolating kid into a SQL query are vulnerable to SQL injection in the same place. Treat kid as untrusted input.
Weak HS256 secret brute force
HS256 is HMAC with a shared secret. If the secret is short or guessable, an attacker who has any valid token can brute force the secret offline (hashcat mode 16500). Once they have the secret, they can mint arbitrary tokens. The fix is RS256 or EdDSA with a real key pair, or HS256 with a 256-bit random secret stored in a real secret manager. Searching for "JWT secret" in any leaked-code dataset is a productive afternoon for an attacker.
Algorithm confusion (HS256 / RS256, "JWK confusion")
A server that supports both HS256 and RS256 has to decide per-token which one to verify with. If it picks the algorithm from the token header (alg) and the key from a configured public key, the attacker can switch alg from RS256 to HS256 and supply a token signed with HMAC-using-the-public-key-as-the-secret. The public key is a known string; the HMAC verifies; the server accepts. Variants embed an attacker-supplied JWK or jku (JWK Set URL) in the header itself, pointing the verifier at the attacker's key. The fix is the same as the alg=none fix: pin the algorithm and the key on the server side, ignore the header's hints.
GraphQL-specific attacks
GraphQL collapses many REST endpoints into one, which collapses many bug classes into one too. The query is a tree, the resolver decides what to fetch per node, and authorisation has to be enforced at every node.
Introspection in production
GraphQL ships with an introspection query that returns the full schema (types, fields, arguments, deprecations). It is invaluable in development and a complete map of the attack surface in production. Disable introspection in production environments, or gate it behind authentication. The trade-off is that legitimate clients that fetch the schema at build time still need it; gate it for unauthenticated callers, not for the whole world.
Query batching abuse
Many GraphQL servers accept an array of queries in a single HTTP request. That collapses N requests into one and makes per-request rate limits useless: one HTTP request can carry a thousand login(email:..., password:...) mutations and brute force a credential at the HTTP layer's rate-limit allowance. The fix is to count complexity at the query layer, not the HTTP layer; libraries like graphql-query-complexity are the standard primitive.
Deep-nested queries and alias amplification
A query like { user(id: 1) { friends { friends { friends { friends { ... } } } } } } can fan out into millions of resolver calls from a single small request. Aliases compound the issue: { a: user(id:1) { ... } b: user(id:2) { ... } c: user(id:3) { ... } ... } lets the attacker pack thousands of object reads into one request that bypasses any per-field rate limit. Enforce a maximum query depth and a maximum query complexity score on every incoming query. Reject anything over the budget before it reaches the resolvers.
Field-level authorisation gaps
Because GraphQL exposes the whole graph through a single endpoint, a single resolver that forgets its authorisation check leaks every object reachable from it. Tools like graphql-shield codify per-field rules; the safer pattern is to push authorisation into the data-fetcher layer (one place to enforce) rather than the resolver layer (every place has to remember).
REST-specific attacks
HTTP verb confusion
Frameworks that route GET /users/{id}/delete to a different handler than DELETE /users/{id} is the obvious case, but the practical one is when middleware applies authorisation only to verbs the developer listed. A route configured to require admin for POST and DELETE but not PUT or PATCH is a real bug; an endpoint that accepts ?_method=DELETE overrides and bypasses CSRF in the process is the same family.
HTTP parameter pollution
The endpoint receives the same parameter twice (?role=user&role=admin) and different layers of the stack pick a different one. The WAF reads the first, the application reads the last, the bug lives in the gap. Common against legacy stacks; still appears in modern stacks behind proxies that merge or split arrays differently from the application server.
Content-type confusion
The endpoint accepts a JSON body but the developer wrote the parser to accept anything. Sending Content-Type: application/xml with an XXE payload, or multipart/form-data with a file upload field that bypasses the JSON validator, or application/x-www-form-urlencoded against an endpoint that expected JSON and binds form parameters onto fields the JSON path would have rejected: each of these is a real bug class. Pin the accepted content types per endpoint; reject everything else with 415.
gRPC and protobuf nuances
gRPC is HTTP/2 with binary protobuf bodies, which sounds like a smaller surface than JSON-over-HTTP and is not. Three properties to keep in mind:
- Protobuf has no required fields by default in proto3. Every field is optional; absent fields take their zero value. Defending against an absent field requires explicit checks, the same shape of bug as mass assignment.
- Protobuf reflection (server reflection) is the gRPC equivalent of GraphQL introspection. Useful for development, a complete service map in production. Disable on internet-facing services.
- The wire format does not distinguish unknown fields from known ones. A client can send extra fields and the server silently drops them; future versions of the server may start interpreting them. Treat schema evolution as a security review boundary.
The same authorisation bugs (BOLA, BFLA) appear with the same shape; the protobuf wire format does not give you better defaults.
Real-world breaches
A short tour of API security incidents in production. For per-case details (exact dates, settlement amounts, ICO/FTC orders), pull the linked source at time of writing.
- Uber 2016: GitHub-leaked AWS keys, attacker pivots into S3. Engineers committed AWS access keys into a private GitHub repo; an attacker found the keys (the exact discovery path has been disputed across multiple disclosures), authenticated to AWS, walked into an S3 bucket, and exfiltrated 57 million users' and drivers' data. The bug is API security in the sense most relevant to API engineering: the credentials that authorise API calls are the security boundary, and committed credentials defeat every other defence behind them. Uber paid the attacker $100,000 through a fake bug-bounty payout and concealed the breach for over a year; the FTC settled in 2018, and a former CSO was later convicted for the cover-up. The lesson on the engineering side is short-lived credentials, vault-issued at runtime, never committed.
- Peloton 2021: no-auth API endpoints returning user data. A researcher (Jan Masters at Pen Test Partners) found that Peloton API endpoints would return age, gender, city, weight, workout statistics, and birthday for any user ID, without requiring authentication. Anybody could enumerate the user ID space and pull profiles in bulk. Peloton's first response was to lock the endpoints to authenticated users only, which still left the BOLA pattern live (any logged-in user could query any other user's data). The classical case of API2 Broken Authentication compounded with API1 BOLA; the fix is per-request authorisation against the caller's identity, not "is there a session at all".
- Facebook 2018: access tokens leaked via View As feature. The "View As" feature, which let users preview their profile as another user would see it, interacted with the video uploader and the access-token issuance flow in a way that issued an access token for the viewed user back to the viewer. About 30 million accounts' access tokens were exfiltrated by attackers using the feature against many targets. The bug is a token-issuance flow that did not bind the issued token to the identity of the calling client; the lesson is that token issuance is an authorisation boundary and has to be reviewed with the same scrutiny as the resource endpoints the tokens unlock.
- Optus 2022: unauthenticated endpoint exposes 9.8 million customer records. An Optus internet-facing API endpoint was reachable without any authentication and returned customer records keyed by an internal ID. An attacker walked the ID space and exfiltrated the records (driver's licence numbers, passport numbers, Medicare numbers, addresses). The Australian Office of the Information Commissioner ran a formal investigation; the public details are at the OAIC investigation report. API1 BOLA layered on API2 Broken Authentication; both at once is the recurring pattern in mass-exfiltration breaches.
Defences worth shipping
The list below is the set of controls I lobby for on every API security engagement, in priority order. Nothing here is novel; the gap is execution.
Short-lived tokens with strict scope
Access tokens that live for minutes, not days. Refresh tokens that live longer but only mint scoped access tokens for one resource at a time. The blast radius of a stolen token is bounded by its lifetime and by what it is allowed to call; both are levers worth pulling. Pin the algorithm and the key on the server side (the JWT section above). Validate aud and iss on every request, not just at issuance.
Authorisation as a first-class layer
BOLA and BFLA are the top two bugs because authorisation is treated as a per-endpoint afterthought. The fix is a single authorisation layer that every data fetcher routes through: an explicit check against the caller's identity, on every object, on every request. Frameworks like Oso, Permify, or hand-rolled policy modules all work; the constant is that authorisation is a separate concern from the handler, with its own tests.
Schema enforcement at runtime via OpenAPI
OpenAPI started as documentation. Treat it as a runtime contract: the gateway or framework rejects any request whose body or query parameters do not match the schema, and rejects any response whose body does not match the schema. The "extra field accidentally in the response" class of data-exposure bug disappears; the "extra field accidentally in the request bound onto the model" class of mass-assignment bug disappears at the same boundary. Tooling: AWS API Gateway request/response validators, Kong's OAS plugin, Envoy's JSON-to-gRPC transcoder, framework middleware (FastAPI's response models, NestJS with class-transformer).
Rate limits at multiple layers
Per-IP at the edge for opportunistic scanners. Per-user at the application layer for authenticated abuse. Per-endpoint for cost-sensitive operations (anything that hits a third party, anything that mints, anything that pays out). Per-flow for the API6 business-flow cases (signup, refer-a-friend, refund). A rate limit at one layer alone is rarely enough; the budget at each layer should reflect the cost of that operation, not a global default.
API gateway in front of every public endpoint
The gateway is where TLS terminates, where authentication is enforced before traffic reaches the application, where rate limits run, where the WAF inspects, where the OpenAPI schema is enforced, and where the request/response is logged for audit. Picking a gateway is less important than having one; every public endpoint, including the half-forgotten v1, has to be behind it.
WAF as a layer, not the layer
A WAF (Cloudflare, AWS WAF, F5, ModSecurity with the OWASP CRS) catches opportunistic injection scanners and credential-stuffing patterns. It does not catch BOLA (a perfectly valid request asking for somebody else's object), does not catch mass assignment (a perfectly valid request body with one extra field), does not catch business-flow abuse (a perfectly valid signup repeated a million times). Use the WAF for the noise floor; build the real defences at the application layer.
Inventory discipline
The API9 fix is documentation as the source of truth. Every endpoint exists in an OpenAPI or protobuf schema in the repo, the gateway is configured from that schema, deprecated endpoints are removed from the gateway when they are deprecated. The discovery layer (Salt Security, Akamai API Security, hand-rolled crawlers against staging) confirms the inventory matches reality.
Where to go next
The API security cluster fans out from this hub. For the tooling side (Burp Suite, Postman with security tests, ZAP API scan, Salt Security, 42Crunch, mitmproxy, Akto, hand-rolled fuzzers), read the best API security tools for 2026 listicle. For the API7 SSRF detail (cloud metadata services, blind variants, defences), read the server-side request forgery deep dive.
For the wider map, back up to the web application security vulnerabilities taxonomy. The API surface is the dominant share of that map in 2026, but the underlying bug shapes (untrusted input, missing authorisation, parser confusion) are the same ones the rest of the cluster covers.
Sources
Authoritative references this article was fact-checked against.
- OWASP API Security Top 10 (2023)owasp.org
- OWASP API1:2023 Broken Object Level Authorizationowasp.org
- OWASP API3:2023 Broken Object Property Level Authorizationowasp.org
- RFC 8725, JSON Web Token Best Current Practicesdatatracker.ietf.org
- CVE-2018-1000531, jjwt alg=none acceptancenvd.nist.gov
- FTC, Uber Technologies, Inc., 2016 breach disclosureftc.gov
- OAIC, Optus 2022 data breach investigationoaic.gov.au





