Writing Secure APIs
Demo Creator
@seed-creator · muallif
The OWASP API Security Top 10
OWASP publishes a dedicated API security list separate from the general web top-10, because APIs fail in their own characteristic ways. Broken object-level authorization (BOLA) leads the list — an attacker swaps an ID in the URL, say /invoices/1042 for /invoices/1043, and reads another user's data. The endpoint authenticated the caller correctly but never checked that the caller owns the object being requested.
The rest of the list rhymes with that theme. Broken function-level authorization lets a regular user call an admin-only route that was never gated. Broken object property-level authorization lets a client update a field — role, balance, is_verified — that should be server-controlled. Unrestricted resource consumption lets one client exhaust your database connections or your monthly bill. Server-side request forgery lets a crafted URL turn your backend into a proxy into your private network. Every one of these is an authorization failure, not an authentication failure.
Never trust the caller: validate ownership on every object access, not just at login.
Authentication Is Not Authorization
Authentication answers "who are you"; authorization answers "are you allowed to do this to this specific thing". A verified token proves identity and nothing more. The single most common API vulnerability is code that stops at authentication — it confirms the JWT is valid, then trusts whatever object id the request names. Ownership and role checks must run on every read and every write, evaluated against the object actually being touched.
Push these checks down to the layer that loads the object, not up in the controller where they are easy to forget. If your repository method takes the current user id and scopes the query — WHERE id = $1 AND owner_id = $2 — then a missing check fails closed with a 404 instead of leaking data. Centralizing authorization in one enforcement point beats scattering it across dozens of handlers that each have to remember.
Prefer returning 404 over 403 for objects the caller may not access. A 403 confirms the resource exists, which is itself an information leak in enumeration attacks. Deny by returning "not found".
Hardening HTTP Headers
Set Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options, and Referrer-Policy on every response. A single misconfigured header can expose the whole app to XSS or clickjacking. Libraries like helmet apply a sane default set in one line, but defaults are a starting point — a real CSP has to be tuned to the exact origins your app loads scripts, styles, and images from.
Content-Security-Policy is the highest-leverage header and the most fiddly. A strict policy that forbids inline scripts neutralizes most reflected and stored XSS even when an injection slips through your output encoding. Roll it out in report-only mode first, collect violation reports for a week, then enforce. HSTS with a long max-age and includeSubDomains prevents downgrade attacks, but only preload it once you are certain every subdomain can serve HTTPS forever.
Logging request bodies for debugging can accidentally capture passwords, tokens, and PII. Redact sensitive fields before writing to any log sink, and treat log storage with the same access controls as the database.
Input Validation and Injection
Validate every input at the trust boundary: type, length, format, and range. Reject anything that does not match an explicit allowlist rather than trying to strip what looks dangerous — blocklists are always incomplete. A validation layer that runs before your business logic turns a class of exploits into boring 400 responses.
Injection follows from unvalidated input reaching an interpreter. Parameterized queries defeat SQL injection because the driver sends data and code on separate channels; the input can never be reinterpreted as syntax. The same principle covers NoSQL, OS command, and LDAP injection: never concatenate untrusted data into a string that some engine will parse. Raw string interpolation into a query is a red flag in code review, full stop.
Rate Limiting and Throttling
Rate-limit by IP and by authenticated user separately. IP-only limits are trivially bypassed by rotating proxies; per-user limits survive IP rotation but do nothing against a fresh-signup flood. You need both, plus tighter limits on expensive or sensitive routes — login, password reset, search, and anything that sends email or money.
Return 429 with a Retry-After header so well-behaved clients back off instead of hammering harder. In a multi-replica deployment, keep the counter in a shared store like Redis; an in-process counter resets on every restart and does not see traffic hitting sibling pods, which quietly defeats the limit under load.