Most public API incidents don’t trace back to a sophisticated or novel technique. They trace back to a well-known control — rate limiting, object-level authorization, input validation — that got deprioritized before launch and never circled back to. This is a checklist of the controls that actually matter, ordered roughly by how often skipping them causes real incidents.
Authentication and object-level authorization
Authenticating a request confirms who’s calling. It says nothing about whether that caller should be allowed to access the specific resource they’re requesting. Broken object-level authorization — where an authenticated user can access another user’s data by changing an ID in the request — remains one of the most common and most exploited API vulnerabilities, precisely because authentication is easy to test and access control on individual resources is easy to forget on any endpoint that wasn’t the main focus during development.
Every endpoint that accepts a resource identifier needs an explicit check that the authenticated caller is actually authorized for that specific resource, not just authenticated in general. This has to be enforced server-side, on every request — never assume that because a UI doesn’t expose a way to request another user’s data, an API consumer can’t construct that request directly.
Rate limiting, applied per-identity and per-endpoint
An API without rate limiting is vulnerable to brute-force credential attacks, scraping, and simple resource exhaustion, regardless of how solid the rest of the security posture is. Rate limits need to be scoped per authenticated identity (or per IP for unauthenticated endpoints), not globally — a global limit lets one abusive client degrade service for everyone else, while a per-identity limit contains the damage to that one caller.
Endpoints that are inherently more sensitive — login, password reset, any action that’s expensive to compute or has real-world side effects — deserve tighter limits than general read endpoints, rather than one uniform rate limit applied everywhere.
Input validation on every parameter, not just the obvious ones
Validate types, formats, and ranges server-side for every input, including ones that feel like internal implementation details — pagination parameters, sort fields, filter values. These are frequently the fields that get skipped because they don’t look security-sensitive, and they’re also where injection and denial-of-service issues (an attacker requesting an enormous page size, or a sort field that maps directly to an unvalidated database column) commonly show up.
Reject unexpected fields rather than silently ignoring them — an API that quietly accepts and processes fields outside its documented schema makes it harder to reason about what the actual attack surface is.
Don’t expose more data than the response needs
A common pattern that causes quiet data exposure: returning an entire internal object or database row because it was convenient, rather than an explicitly defined response schema. This routinely leaks internal fields — hashed passwords, internal flags, other users’ identifiers embedded in nested objects — that were never meant to be public, simply because nobody defined what should and shouldn’t be in the response.
TLS, and actually enforcing it
HTTPS-only sounds obvious, but “supports HTTPS” and “rejects plaintext HTTP entirely” are different configurations, and it’s common for an API to accept both silently, quietly downgrading security for any client (or misconfigured integration) that happens to connect over HTTP. Enforce it at the load balancer or gateway level, not as an assumption about how clients will behave.
Logging and monitoring built for incident response, not just uptime
Standard uptime and error-rate monitoring won’t catch a slow, low-and-slow scraping attack or a credential-stuffing campaign hiding inside otherwise-normal traffic patterns. Log enough detail — caller identity, endpoint, response code, timestamp — to reconstruct what happened during an investigation, and specifically monitor for patterns like unusual volumes of authorization failures or an unusual spread of resource IDs being requested by a single identity.
The checklist
- Object-level authorization enforced server-side on every resource-accessing endpoint, not just authentication
- Rate limits scoped per identity and tightened further on sensitive endpoints
- Server-side validation on every input parameter, including pagination, sorting, and filtering fields
- Explicit response schemas, never raw internal objects
- HTTPS enforced, not just supported
- Logging detailed enough to reconstruct an incident after the fact
None of these are exotic. They’re the controls that get cut when a launch deadline is tight, on the assumption they can be added later — and “later” is usually after the first incident that specifically exploited whichever one got skipped.
