The OWASP API Security Top 10 lists the most common and damaging ways APIs fail, published by the Open Worldwide Application Security Project (OWASP). You can turn those risks into engineering tasks by working through the checklist. It covers requiring authentication on every endpoint, authorizing every object access, rate limiting requests, validating input against a schema, keeping secrets out of code and logs, and testing for abuse before attackers do.
The stakes justify the effort. Salt Security’s 2024 State of API Security Report found that 95% of organizations experienced an API security problem in the past 12 months. Public breaches follow the same pattern: T-Mobile lost 37 million records through an API in 2023, and Optus lost 9.8 million through an unauthenticated endpoint in 2022.
The common thread in those incidents is a missing check. An endpoint trusted a client-supplied ID. A token was never verified. A rate limit existed on only one server. Working through the checklist in order closes those gaps. For each control you will see what to implement and how to verify it holds.
Authentication: verify identity on every request
Authentication establishes who is calling. For APIs, that usually means an OAuth or OpenID Connect flow that issues a token, and the API verifies that token on every request. The current best practice guidance, RFC 9700 (BCP 240), requires public clients such as single-page apps and mobile apps to use PKCE, a protocol that stops an attacker who intercepts an authorization code from exchanging it for a token, because the attacker would also need a code verifier that only the legitimate client holds. It also forbids the resource owner password credentials grant, in which the app collects the user’s password directly, and recommends tokens that are restricted to a specific API audience and bound to a specific client, using mutual TLS (mTLS) or DPoP.
Tokens are commonly JWTs, JSON Web Tokens, which carry signed claims about the user. With DPoP (Demonstrating Proof of Possession), the client sends a DPoP header containing a JWT signed by its private key, and the access token carries a thumbprint of that key. A DPoP-bound token presented as a plain Bearer token must be rejected, because that presentation means the token left the client it was issued to.
Validation is where implementations fail. The server must reject tokens with an alg of none, take the signing algorithm from its own configuration rather than from the token header, and check the issuer (iss), the audience (aud) that names your API, and the expiry (exp) time. A simple test exposes a broken verifier: take a valid token, change a claim, and send it. If the server accepts it, the signature is not being verified, and that is a critical vulnerability.
Refresh tokens need their own controls. Rotate the refresh token on every use, and track a session family ID plus device ID and app version so you can distinguish a genuine token leak from a client replay such as an iPhone backup and restore. When a reused refresh token arrives, revoke the session family and alert.
One OpenAPI review finding shows up constantly: an empty security requirement, written as {} in the operation definition, makes authentication optional for that operation. During review, search the spec for operations that declare {} where they should declare an OAuth or API key requirement. Every endpoint should declare a non-empty security requirement.
Long-lived API keys issued to mobile apps or browser front ends are public by construction. Assume an attacker will extract them, and treat them as client identifiers, not as privileged credentials.
Authorization: check ownership in the application
Authorization answers a different question: given a verified caller, may they perform this action on this specific resource? The number one risk in the OWASP API Security Top 10 is broken object level authorization, BOLA. It happens when an endpoint accepts a client-supplied ID, uses it to fetch a record, and never checks that the caller may access that record. An attacker changes the ID in the URL or body and reads another user’s data. BOLA is trivially exploitable and nearly invisible to most automated scanners, because the request looks completely normal.
No API gateway can fully fix BOLA or the two related authorization failures, because only the code that loads the record knows who owns it. A gateway sees a token and a path; it does not know which user owns a given record.
The fix is a rule applied at every function that uses a client-supplied ID: confirm the caller may perform that action on that record, using the identity from the token subject, never from a request parameter. The token subject is the authenticated user identifier from the verified token. If the code reads an owner ID from the request body or query string, an attacker simply changes it.
The second failure is broken function level authorization, BFLA: a caller invokes an operation they were never granted. The safe default is to deny every route and HTTP method unless it is explicitly allowlisted, return 405 Method Not Allowed for anything else, and put administrative functions on a separate API, host, or audience so a normal user token cannot even reach them.
The third failure is broken property level authorization, BOPLA: an endpoint returns or updates more fields than the caller should see or change. A profile endpoint might return an internal role or credit balance alongside the public profile. The fix is explicit response and update allowlists: the code names the fields it will return and the fields it will accept for update. Generic serializers that dump the whole object, and auto-binding frameworks that map any request field onto a model, make BOPLA possible.
Rate limiting: enforce quotas per consumer
A rate limiter controls how many requests a consumer can make within a defined time window. It protects backend services from overload, stops one client from consuming shared capacity, and keeps usage predictable while controlling cost.
Fixed window divides time into fixed intervals, so a client with a limit of 100 requests per minute can send 100 requests at the end of one minute and another 100 at the start of the next, a burst of 200 in a short span. Sliding window tracks requests over a rolling period and reduces that boundary burst. Leaky bucket places requests into a bounded queue that empties at a steady rate, which smooths bursts but adds latency. Token bucket adds tokens at a fixed rate up to a capacity, allowing bounded bursts while the refill rate controls long-term traffic.
When a client exceeds the limit, return HTTP 429 Too Many Requests with a clear error message, and include the Retry-After header when you can estimate a retry time. Clients should honor Retry-After; when it is absent, they should use exponential backoff with jitter and a retry cap so they do not all retry at the same moment. If you return a generic 500 error instead of 429, clients treat it as an infrastructure failure and retry immediately, compounding the overload.
In a multi-instance deployment, rate limiting must use shared storage such as Redis or a dedicated API gateway. If each instance keeps a local counter, the effective limit multiplies by the number of instances, so a distributed client can exceed the intended threshold by spreading requests across nodes.
A single global limit without per-consumer isolation has the opposite problem: one high-volume client can drain the shared quota for everyone else. Apply limits per consumer, typically per API key or per authenticated user, and tier them by plan if you offer different subscription levels. A weather API, for example, might allow free users 100 requests per day and premium subscribers 10,000 requests per day.
Input validation: treat the OpenAPI document as a contract
The validation layer rejects requests that do not match what the API is supposed to accept. The most effective way to enforce this at scale is to treat the OpenAPI document as a contract and validate every request and response against its schema, at the gateway or in code.
OpenAPI is the standard format for describing an API’s endpoints, parameters, and data shapes. Its schema language is JSON Schema, the foundation specification that OpenAPI and AsyncAPI build on. JSON Schema defines types, lengths, array sizes, and ranges in a machine-checkable way, which is what makes contract validation possible.
The validation layer should reject unknown paths, unknown methods, and unknown properties, not just check the fields you care about. Enforce types, lengths, array sizes, and ranges. Return 413 Payload Too Large for oversized bodies and 415 Unsupported Media Type for an unexpected Content-Type. If an undocumented endpoint exists, that is a shadow API, and shadow APIs escape every control applied to the documented ones. Attackers find them by enumerating paths, which is why the OpenAPI document and the deployed routes must match.
Injection applies to APIs in its own shape. SQL injection, NoSQL injection, and command injection all work through API parameters, but the payloads must be JSON-aware: they have to survive JSON escaping before they reach the query builder. Endpoints that fetch a URL the client provides are a separate risk: an attacker can use them to make the server reach internal services, a server-side request forgery (SSRF) pattern.
Pagination and GraphQL are where input validation meets rate limiting. A request with page_size=999999 can force the server to load a huge dataset, a resource exhaustion vulnerability. In GraphQL, aliases let a client pack hundreds of queries into a single request, so per-request limits do not cap the actual work. If you expose GraphQL, count field resolution or query cost, not just requests.
Secrets: keep credentials out of code and logs
Keep credentials out of code and logs, and rotate them when they leak. The most common leak is version control. Millions of active API keys end up committed to Git repositories every year, and a private repository does not protect a committed secret. Private repos get cloned onto laptops and CI runners, and git history is immutable, so the secret remains readable permanently even after you delete it from the working tree.
Framework defaults create leaks in unexpected places. Next.js automatically inlines environment variables prefixed with NEXT_PUBLIC_ into client-side JavaScript bundles, so a variable named NEXT_PUBLIC_STRIPE_SECRET_KEY ships to every user’s browser. The prefix means "safe to expose to the browser", and a secret key is not.
Catch secrets at commit time and at startup, before they spread. Use Gitleaks as a pre-commit hook via Husky so the hook blocks a commit that contains a hardcoded secret. At application startup, validate the required environment variables with a runtime schema validator such as Zod, so the service fails fast when a secret is missing or malformed instead of starting with a null credential. In CI/CD, use OpenID Connect instead of storing static cloud keys: GitHub issues a short-lived token, and the cloud provider assumes a temporary role valid for 15 minutes, so there is no long-lived credential to steal. In Docker builds, use BuildKit secret mounts (--mount=type=secret) so credentials are available during the build but never baked into image layers; ARG and ENV do bake them in.
If a secret does get committed, revoke the credential immediately, audit the logs for unauthorized access, then purge history with git-filter-repo and force-push. Revocation comes first because rewriting history does not undo the exposure. Anyone who cloned the repository before the purge still has the secret.
You can rotate keys without downtime using a dual-key pattern: accept an array of valid keys for verification, but sign new tokens with the newest key. Existing tokens keep working during the transition window, and once clients have picked up the new key, remove the old one from the verification array.
Logs are a quieter leak channel. JWTs and authorization headers that land in logs can be replayed. Redact authorization headers in the logger middleware, and also configure the log sink, for example Splunk, so fields such as authorization, cookie, and set-cookie are masked or dropped at ingest. Some component will log a header anyway; the sink is the backstop.
Abuse testing: prove the controls hold
Tests turn the checklist into evidence. A practical API security testing program follows five steps: inventory all endpoints, prioritize by data sensitivity, test critical APIs for BOLA and authentication bypass, automate the tests in CI/CD, and monitor in production.
Inventory is the step most teams skip. You cannot test what you do not know exists. Build the inventory from the OpenAPI document and from traffic logs, and treat the traffic as the truth about what is deployed, because the two lists will disagree. Every undocumented endpoint is a candidate for the next breach.
BOLA testing needs at least two user accounts with different access levels. Log in as user A, capture a request that references a record owned by user A, then replay that request with user B’s token. If the API returns the record, BOLA is confirmed. Run this test on every feature that takes an ID.
Token tests cover expiry, revocation, and tampered claims. Take a valid token and change a claim, set the algorithm header to none, or present an expired token, and confirm the server rejects each one. If any variant passes, the authentication layer is broken. Rate limit tests should fire a burst past the configured limit and verify the API returns 429 and enforces the window, per endpoint and per user, including pagination abuse such as an extreme page size.
Generic DAST tools, dynamic application security testing scanners built for web applications, miss API-specific risks. They are not built to understand object ownership or the order of operations, so BOLA, broken function-level authorization, and business logic abuse pass right through them. You need tests written against your API. OWASP OFFAT fuzzes inputs from a YAML config, Autoswagger detects broken authorization by hitting endpoints without proper auth, and OWASP ZAP integrated with Playwright can authenticate once, run spider and active scans, and generate HTML and CSV reports on every pipeline run. Business logic abuse, a sequence of legitimate requests used in a harmful order, needs detection that understands the sequence, because a static scanner cannot see that apply discount, refund, apply discount again is an attack.