The shape of API security
Most "API security" advice boils down to a handful of categories that compose. Each one has its own controls, its own failure modes, its own trade-offs. The categories:
- Transport security. Protect data in motion.
- Authentication. Identify the caller.
- Authorization. Decide what the caller is allowed to do.
- Input validation. Reject malformed or hostile inputs before they reach business logic.
- Output handling. Don't leak data the caller shouldn't see.
- Rate limiting and abuse prevention. Limit damage from misuse.
- Secret management. Keep credentials safe operationally.
- Logging and incident response. Detect and recover from compromise.
Every public API needs all eight. Skipping any one of them tends to produce the kind of incident that ends up in the news.
Transport security
HTTPS is mandatory for every public API in 2026. Without it, anyone on the network path can read every request and response, modify them in flight, and impersonate either side. There is no defensible reason to expose an API over plain HTTP.
Three details that catch teams out:
- HSTS. The
Strict-Transport-Securityresponse header tells browsers to always use HTTPS for the domain, even if a user typeshttp://. Without HSTS, an attacker on the network can intercept the first plain-HTTP request before the redirect to HTTPS. - TLS version and cipher selection. Disable TLS 1.0 and 1.1. Disable RC4, 3DES, MD5. Use TLS 1.2 with modern ciphers as the floor; TLS 1.3 where supported. Most cloud load balancers have a "modern" preset that gets this right; review it once a year.
- Certificate management. Expired certificates take down APIs more often than any other single cause. Automate renewal (Let's Encrypt + cert-manager, ACM, similar). Set up monitoring that pages on impending expiry. Test the renewal pipeline in staging.
Authentication
Every endpoint needs to know who is calling. The mechanisms — API keys, OAuth 2.0, JWT, mTLS — and their trade-offs are covered in the API authentication reference. The implementation patterns — secret storage, rotation, session lifecycle, recovery from leaks — are in the authentication guide.
Three principles that hold across mechanisms:
- Authenticate at the edge. The first thing your request pipeline does is identify the caller. Code that runs after that point should never see an unauthenticated request, never have to guess at identity. Centralizing authentication in middleware (or a gateway) prevents the "this one endpoint forgot to check" class of bug.
- Distinguish "no credentials" from "wrong credentials" from "valid but expired" — server-side, in your logs. The client should see a generic "401" for all three; you should be able to tell them apart for debugging and detection.
- Rate-limit authentication separately. A flood of failed login attempts is a credential-stuffing attack. Rate-limit by source IP and by attempted identity, separately, with stricter limits than for normal API calls.
Authorization
Authorization is the harder problem. Authentication is mostly a solved engineering problem; authorization is a business-logic problem dressed up as a security one, and it's where most API security incidents actually originate.
Two kinds of access control
- Object-level (or "row-level") authorization. Can this user access this specific resource? "Can Alice see order #12345?" The right answer depends on whether order #12345 belongs to her. The bug pattern: the endpoint checks that Alice is authenticated, fetches the order by ID from the request, and returns it — without checking that the order is hers. This is the most common API security bug. OWASP calls it "Broken Object Level Authorization" (BOLA / IDOR), and it's the #1 entry on the OWASP API Security Top 10 for a reason.
- Function-level authorization. Can this user perform this kind of action? "Can a regular customer access the admin endpoints?" Usually checked via roles or scopes. Easier to get right because there are fewer permissions to check, but the cost of getting it wrong is higher (admin actions are powerful).
Defending against object-level bugs
The pattern that prevents this whole class of bug: never trust an ID in the request without checking that the authenticated identity has access to it. The pattern in code:
function getOrder(request, orderId):
user = request.authenticatedUser
order = db.findOrder(orderId)
if order is null:
return 404
if order.customerId != user.id and not user.isAdmin:
return 404 # not 403 — don't confirm the order exists
return order
Two things worth noticing. First, the authorization check happens after the fetch but before the return — you have to look up the resource to know who owns it. Second, the response for "exists but not yours" is 404, not 403, because returning 403 leaks the existence of the resource. For non-sensitive resources 403 is fine; for anything sensitive, prefer 404.
For systems with many endpoints, push the check down: every database query for a resource includes the owner ID as a filter, so a missed authorization check returns no rows rather than wrong rows. Centralizing the check in a query layer is far safer than relying on every endpoint to remember.
Avoiding privilege creep
Long-lived tokens accumulate scopes. A user who once needed admin access for a task and got their session updated keeps it after the task is done. The defense: explicit scope grants per session, time-bounded, with audit logging when high-privilege scopes are issued. Periodic review of who has what.
Input validation
Every input from a client is hostile until proven otherwise. Validation isn't just about catching mistakes; it's about preventing inputs from reaching code that wasn't designed for them.
What to validate
- Type. If your handler expects an integer, reject a string at the boundary. JSON parsers will happily give you whatever was in the JSON; your validation layer should normalize before business logic sees it.
- Range and format. Email addresses match the expected format; amounts are positive; dates are in expected ranges; enum fields contain only allowed values.
- Length. Cap string lengths, array lengths, nested object depths. Without caps, a single request can consume megabytes of memory parsing.
- Business invariants. The user can't transfer more than their balance. The order can't ship before it's paid. These belong inside business logic, but the simpler checks should fail fast at the boundary.
Schema-driven validation
The pattern that scales: define request schemas in a machine-readable format (JSON Schema, OpenAPI, GraphQL SDL) and let a middleware reject malformed requests before any handler sees them. The same schema documents the API, generates client code, and feeds tests. The cost of writing schemas is paid back many times over.
Injection attacks
The classic injection bugs — SQL injection, command injection, header injection — happen when untrusted input gets concatenated into something the receiver interprets as code or instructions. The defenses:
- Parameterized queries for SQL. Never concatenate user input into a query string, no matter how clever the escaping.
- Subprocess argument arrays for shell commands.
exec(["ls", userPath]), neverexec("ls " + userPath). - Header-value sanitization when echoing user input into response headers. CRLF in user input becomes a new header in the response if you don't strip it.
- Strict templating for any text that user input might end up in. Templates that auto-escape (Jinja2, ERB with default settings) are safer than string concatenation.
Output handling
Two sides to this:
- Don't return more data than the caller asked for. A user fetching their profile shouldn't get back the password hash. An admin endpoint shouldn't return internal IDs that aren't supposed to be exposed. Define what each endpoint returns explicitly; don't serialize the full database row.
- Don't leak data through error messages. "User [email protected] not found" tells the caller whether that email is registered. Generic error responses (and detailed server-side logs) are the right pattern.
The pattern: output schemas separate from input schemas, both versioned. Each response is built from explicit field selection; nothing leaks by accident.
Rate limiting and abuse prevention
Without rate limits, a single bad actor can take down your API. With rate limits, the damage is bounded to what one rate-limit window can do. Three layers:
- Per-IP at the edge. Cheap, defends against unauthenticated traffic floods. Coarse — an entire NAT can share an IP — so don't rely on it as the only signal.
- Per-credential at the application. The right place for billing-aligned limits and per-tenant fairness.
- Per-action. Expensive endpoints (bulk operations, search) get tighter limits than cheap ones. Authentication endpoints get the tightest limits because credential-stuffing attacks live there.
The algorithms — fixed window, sliding window, token bucket, leaky bucket — and the trade-offs between them are covered in API Rate Limiting Strategies.
Secret management
Secrets are credentials, signing keys, encryption keys, anything an attacker would want. The operational discipline:
- Never in source code. Not even in private repositories. Use a secrets manager.
- Never in URLs or logs. Always in headers, request bodies, or environment variables.
- Rotate on a schedule, faster on a leak. Build the rotation procedure before you need it; the worst time to invent it is during an incident.
- Scope minimally. A credential that lets one service talk to one other service is far safer than a master key with access to everything. The blast radius of a leak is the sum of what the credential can reach.
- Audit access. Who pulled which secret when. A leaked secret with no audit trail is much harder to investigate than one with a clear access log.
Practical patterns are in the authentication guide.
Logging and incident response
Security logging serves two purposes: detecting attacks in progress, and reconstructing what happened during an incident. Both depend on having logs that are detailed enough, retained long enough, and queryable enough to be useful.
What to log
- Authentication attempts (success and failure), with source IP, user-agent, timestamp, and the identity attempted.
- Authorization failures (403s), with the requested resource and the authenticated identity.
- Privileged operations: admin actions, credential issuance, role changes, data exports.
- Rate-limit triggers — bursts of 429s often precede or accompany attacks.
- Per-request correlation IDs that follow the request through every system, so you can reconstruct the full path from one log entry.
What not to log
- Full credentials, tokens, passwords. Truncate or hash before logging.
- Personally identifiable information beyond what's needed for the security purpose. The log itself becomes a target.
- Full request/response bodies for endpoints that handle sensitive data.
Detection
Logs are useless if no one looks at them. The minimum: alerts on patterns that indicate compromise — a single source attempting many authentication failures, a single credential succeeding from many IPs in a short window, a privileged operation by an account that's never used it before. Automated detection that pages a human is the difference between catching an incident in minutes and catching it in months.
Disclosure and response
Have a security contact published — typically security@yourdomain and a /.well-known/security.txt file. Have a response procedure documented before you need it: who decides whether to notify customers, who handles the investigation, who talks to the press if needed.
Common mistakes
- Object-level authorization checked once and assumed everywhere. If your endpoints take resource IDs as parameters, the check has to happen on every fetch.
- Mass assignment. Accepting a JSON object and updating every field on the database row that matches a key. Attackers send
{"isAdmin": true}; your ORM happily writes it. Whitelist the fields a request is allowed to change. - CORS misconfiguration.
Access-Control-Allow-Origin: *combined withAccess-Control-Allow-Credentials: trueis a serious vulnerability. Echo the origin only after validating it against an allow-list. - Mixing internal and external APIs on the same surface. Internal endpoints deserve different security models. Expose them on a separate hostname or behind authentication that's never accepted on the public surface.
- JWT verification bugs. Trusting the algorithm field, not pinning the expected algorithm, accepting
alg: none. The auth reference covers these in detail. - Long-lived credentials with no rotation. The credential you can't rotate is the credential you can't recover from when it leaks.
- No rate limit on authentication. Credential-stuffing attacks succeed against APIs that rate-limit normal traffic but not login.
- Hiding behind security through obscurity. Unguessable URLs are not a security control; treat them as public unless they're behind authentication.
Where to go next
For the protocol-level reference on credentials, see API Authentication. For implementation patterns including rotation and recovery, see the authentication guide. For rate limiting, see API Rate Limiting Strategies. For the broader API design context that security sits inside, see API design best practices.