API Security Checklist
The controls that keep an API from becoming a breach — auth, authorization, input handling, transport, and the OWASP API Top-10 traps.
API security comes down to answering two questions on every request — who is calling (authentication) and may they do this, to this object (authorization) — and then treating everything in the request as hostile input. Most real breaches are not broken cryptography; they are a missing ownership check on an endpoint someone forgot existed.
The sections run from identity to inputs to operations, and the OWASP API Security Top 10 (2023) maps each risk to its fix. Use the sheet as a review checklist before launch, and again whenever you add an endpoint.
Authentication
- Every non-public route
- Require a verified identity — no "internal" endpoint left open.
- Tokens
- Short-lived access tokens + refresh; verify signature, issuer, audience, expiry.
- Secrets
- Never hardcode. Use a vault/KMS/env; rotate; never log tokens or keys.
- Cookies
HttpOnly; Secure; SameSite. Protect state-changing routes from CSRF.- JWT validation
- pin the expected algorithm, reject
alg: none, checkiss,aud,exp,nbf - Algorithm confusion
- an RS256 public key accepted as an HS256 secret lets anyone forge tokens
- OAuth 2.0 + PKCE
- authorization code with PKCE for browser and mobile apps — never the implicit flow
- API keys
- store only a hash, show the key once, add a scannable prefix, support rotation
- Passwords
- hash with Argon2id, scrypt or bcrypt — never a fast hash such as SHA-256
- Credential stuffing
- rate-limit logins per account and IP, offer MFA, reject known-breached passwords
- Service to service
- mTLS or short-lived workload identity tokens instead of shared static secrets
Authorization (the #1 API risk)
- Object-level (BOLA)
- Check the caller owns the object —
GET /orders/123must verify 123 is theirs. #1 OWASP API risk. - Function-level
- Enforce roles per action — don’t rely on the UI hiding admin buttons.
- Deny by default
- Allowlist what’s allowed; new endpoints start locked.
- Mass assignment
- Bind only allowed fields — never let a request set
roleorisAdmin. - Property level (BOPLA)
- return only fields the caller may see, accept only fields they may change
- Server-side, every request
- enforce it in middleware or the data layer — never only in the client or gateway
- Unguessable IDs
- UUIDs slow enumeration but never replace an ownership check
- Tenant isolation
- scope every query by tenant id in one place, such as row-level security
Input & output
- Validate everything
- Schema-validate body, query, params. Reject unknown fields; bound sizes.
- Parameterized queries
- Always — the fix for SQL/NoSQL injection. Never string-concat inputs.
- Encode output
- Context-aware encoding to stop XSS; set
Content-Typeexplicitly. - Rate limit & quota
- Per-key/IP limits + payload size caps — blunt scraping, brute force, and DoS.
- SSRF
- allowlist outbound hosts; block private ranges and
169.254.169.254; require IMDSv2 - Pagination caps
- enforce a maximum page size on the server —
?limit=1000000is a DoS - GraphQL limits
- cap query depth, complexity and batching; disable introspection in production
- File uploads
- check size and type by content, rename, store outside the web root, scan before serving
- Sensitive responses
Cache-Control: no-store, so tokens and PII never land in shared caches
Transport & hygiene
- TLS only
- HTTPS everywhere + HSTS. Redirect HTTP → HTTPS.
- Minimal errors
- Generic error messages to clients; details to logs. No stack traces in responses.
- CORS narrowly
- Explicit allowed origins; never
*with credentials. - Security headers
- CSP,
X-Content-Type-Options: nosniff,X-Frame-Options. - Log & monitor
- Audit auth failures and anomalies; alert on spikes. Don’t log secrets/PII.
- TLS versions
- TLS 1.2 at minimum, 1.3 preferred; switch off legacy protocols and weak ciphers
OWASP API Top 10 (2023)
- API1 BOLA
- Broken Object Level Authorization — no ownership check on an object id
- API2
- Broken Authentication — weak login, token or session handling
- API3 BOPLA
- Broken Object Property Level Authorization — excessive data exposure plus mass assignment
- API4
- Unrestricted Resource Consumption — no limits on size, rate, cost or third-party spend
- API5
- Broken Function Level Authorization — admin functions reachable by regular users
- API6
- Unrestricted Access to Sensitive Business Flows — bots buying out stock, mass sign-ups
- API7
- Server Side Request Forgery — the API fetches a URL the attacker chose
- API8
- Security Misconfiguration — verbose errors, open CORS, missing headers, default settings
- API9
- Improper Inventory Management — forgotten old versions and undocumented endpoints
- API10
- Unsafe Consumption of APIs — blindly trusting what third-party APIs return
Webhooks & integrations
- Sign webhooks
- HMAC-SHA256 over the timestamp plus the raw body, with a per-endpoint secret
- Verify on receipt
- recompute the HMAC over the raw bytes and compare in constant time
- Replay window
- reject timestamps older than about five minutes and deduplicate event ids
- Outbound calls
- timeouts, response size limits and schema validation on every third-party API
Testing & operations
- Two-account test
- call every endpoint with user B’s token against user A’s objects — the BOLA test
- API inventory
- an up-to-date OpenAPI spec per version, and a schedule for retiring old ones
- Secret scanning
- scan commits and CI logs for keys; rotate immediately after any leak
- Dependency scanning
- track CVEs in libraries and base images and patch on a fixed timeline
- DAST and pentests
- automated scans in CI, plus a manual test before major launches
Recipes
Verify a webhook signature with a replay window
import hashlib, hmac, time
def verify_webhook(raw_body: bytes, timestamp: str, signature: str, secret: bytes) -> bool:
if abs(time.time() - int(timestamp)) > 300: # older than 5 minutes: possible replay
return False
signed = timestamp.encode() + b"." + raw_body # sign exactly what was sent
expected = hmac.new(secret, signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature) # constant-time comparisonVerify the raw bytes before parsing: re-serializing the JSON changes spacing and breaks the signature. Also store processed event ids, so a replay inside the window is rejected too.
Test for BOLA with two accounts
1. create users A and B; as A, create an order -> id 1042
2. as B: GET /orders/1042 -> expect 404 (or 403), never 200
3. as B: PATCH /orders/1042 {...} -> expect 404 / 403, and nothing changed
4. as B: DELETE /orders/1042 -> expect 404 / 403
5. repeat for every id the API accepts: in the path, query string, body and headers
6. automate it: one negative test per endpoint, run in CIScanners rarely find BOLA because the responses look normal; only a test that knows who owns what can. Include ids nested inside request bodies, which are the most often missed.
Validate a JWT, in order
1. parse the header; reject alg "none" and any algorithm you did not configure
2. pick the key by kid from your own JWKS, never from a URL inside the token
3. verify the signature with that key and the expected algorithm
4. check iss (who issued it) and aud (that it is meant for this API)
5. check exp and nbf, allowing about 60 seconds of clock skew
6. only then trust the claims: subject, scopes, tenantUse a maintained library and configure the allowed algorithms explicitly. Skipping the aud check lets a token issued for one of your services be replayed against another.
Rate limits that blunt abuse
per API key: 1,000 requests per minute, token bucket (short bursts allowed)
per IP: login 5 attempts per minute, then an exponential lockout
per account: password reset 3 per hour
over the limit: 429 Too Many Requests + Retry-After: 30
expensive ops: separate, lower quotas for exports, search and LLM calls, plus payload capsLimit on the identity that matters: per key for API abuse, per account for credential attacks, since attackers rotate IPs. Return the same response for unknown and wrong passwords so limits cannot be used to enumerate users.
Check security headers and CORS from the shell
curl -sI https://api.example.com \
| grep -iE 'strict-transport|content-security|x-content-type|x-frame|cache-control'
curl -s -o /dev/null -w '%{http_code}\n' http://api.example.com # expect 301/308 to https
curl -sI -H 'Origin: https://evil.example' https://api.example.com \
| grep -i access-control-allow-origin # should print nothingAn API that echoes any Origin back in Access-Control-Allow-Origin, especially with credentials allowed, lets other websites read responses as your logged-in users.
Questions about API Security Checklist
What is BOLA (Broken Object Level Authorization)?
BOLA is the number one risk in the OWASP API Security Top 10. It happens when an API takes an object identifier from the request, such as an order id in the URL, and returns or changes that object without checking that the caller is allowed to access it. An attacker simply changes the id, from 1042 to 1043, and reads someone else’s data. The fix is an ownership or permission check on every request that touches an object, enforced on the server, typically by scoping database queries to the authenticated user or tenant. Unguessable ids help but are not a fix.
What is the OWASP API Security Top 10?
It is a list, maintained by the OWASP Foundation, of the most critical security risks specific to APIs; the current edition was published in 2023. It covers broken object level authorization, broken authentication, broken object property level authorization, unrestricted resource consumption, broken function level authorization, unrestricted access to sensitive business flows, server side request forgery, security misconfiguration, improper inventory management, and unsafe consumption of third-party APIs. Most of the list is about authorization and limits rather than cryptography, which matches where real API breaches happen. Teams use it as a design and code review checklist.
Should I use API keys or OAuth?
Use API keys for server-to-server access where the caller is a system you trust to keep a secret, such as a customer’s backend calling your API: they are simple, but long-lived and tied to an account rather than a user. Use OAuth 2.0 when an application acts on behalf of a user, when you need scoped, revocable and short-lived access tokens, or when third-party apps connect to your platform. Browser and mobile apps should use the authorization code flow with PKCE. Many platforms offer both, and either way keys and tokens belong in headers, never in URLs.
What is the difference between authentication and authorization?
Authentication establishes who the caller is, for example by validating a password, an API key, a session cookie or a signed token. Authorization decides what that authenticated caller may do: which endpoints they can call, which objects they can read or change, and which fields they can see or edit. A request can pass authentication and still be forbidden. A failed authentication should return 401, a failed authorization 403, or 404 when you do not want to reveal that a resource exists. Most serious API vulnerabilities are authorization gaps, so check it on every request, for every object.