API Design
05 / 05

API Security

API Design: API Security

OWASP API Security Top 10 identifies the most critical API vulnerabilities. Understanding these is essential for designing safe APIs.

OWASP API Security Top 10

  • API1 — Broken Object Level Authorization (BOLA/IDOR): always verify the current user owns the resource. Never trust client-provided IDs without checking ownership. GET /orders/123 must verify order 123 belongs to the authenticated user.

  • API2 — Broken Authentication: use short-lived tokens (15min access, 7d refresh), rotate refresh tokens, rate-limit login attempts, use secure cookie attributes (HttpOnly, Secure, SameSite=Strict).

  • API3 — Broken Object Property Level Authorization: never use mass assignment. Whitelist which fields clients can set. PATCH /users/:id must not allow setting role or isAdmin.

  • API4 — Unrestricted Resource Consumption: rate-limit all endpoints, limit request body size, paginate all lists, add query complexity limits in GraphQL, add timeouts to all DB queries.

  • API5 — Broken Function Level Authorization: admin endpoints must check role, not just authentication. Test every privileged endpoint with a non-admin token.

  • API6 — Unrestricted Access to Sensitive Business Flows: limit password resets, OTP requests, payment attempts per user per time window. Detect and block automated flows.

  • API7 — Server Side Request Forgery (SSRF): validate and allowlist URLs when your API fetches external resources. Never pass raw user-supplied URLs to HTTP clients.

  • API8 — Security Misconfiguration: disable debug endpoints in production, remove CORS wildcard (*) for authenticated APIs, set security headers, disable unnecessary HTTP methods.

  • API9 — Improper Inventory Management: document all APIs, decommission old versions, don't expose beta/internal APIs without auth, monitor for shadow APIs.

  • API10 — Unsafe Consumption of APIs: validate and sanitize all data from third-party APIs before using it. Third parties can be compromised.

CORS

# CORS (Cross-Origin Resource Sharing) — browser enforces, server configures

# Simple request: browser sends directly, server must respond with headers
# Preflight: browser sends OPTIONS first for non-simple requests

# Response headers:
Access-Control-Allow-Origin: https://app.example.com    # ✅ specific origin
Access-Control-Allow-Origin: *                          # ❌ never for authenticated APIs
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Allow-Credentials: true                  # required for cookies/auth headers
Access-Control-Max-Age: 86400                           # cache preflight for 24h

# Common mistakes:
# - Reflecting back any Origin header without validation (open CORS)
# - Combining * with Allow-Credentials (browsers reject this)
# - Not handling OPTIONS preflight (returns 404/405)

Authentication & Token Security

# JWT storage — where to keep tokens in the browser

LocalStorage / SessionStorage:
  ✅ Easy to use, accessible from JS
  ❌ XSS vulnerable — any injected script can read it
  ❌ Never store long-lived tokens here

HttpOnly Cookie:
  ✅ Inaccessible to JS — XSS cannot steal it
  ✅ Automatically sent with requests
  ❌ CSRF vulnerable — add CSRF token or use SameSite=Strict

Recommended: HttpOnly Secure SameSite=Strict cookie for refresh tokens
             Bearer header (short-lived access token) from memory (not localStorage)

# Security headers every API should set:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Content-Security-Policy: default-src 'none'
Referrer-Policy: strict-origin-when-cross-origin

Rate Limiting Strategies

# Fixed Window — count requests in a fixed time window
Limit: 100 requests / 60 seconds
Problem: burst at window boundary (200 requests in 2 seconds straddling the window)

# Sliding Window — rolling window, no boundary burst
More accurate, more memory usage (store per-user timestamps)

# Token Bucket — bucket refills at a rate, burst allowed up to bucket size
bucket_size: 20, refill_rate: 10/second
Allows short bursts (20 at once), then throttles to 10/s sustained

# Leaky Bucket — requests process at fixed rate, excess queued or dropped
Smooth output rate, good for downstream protection

# Implementation tiers:
Per-IP: coarse-grained, spoofable (X-Forwarded-For)
Per-user: after auth, accurate for logged-in users
Per-API-key: for server-to-server, track per key
Per-endpoint: tighter limits on expensive endpoints (search, export)

Input Validation & Injection Prevention

# SQL Injection — NEVER concatenate user input into SQL
❌ db.query("SELECT * FROM users WHERE name = '" + name + "'")
✅ db.query("SELECT * FROM users WHERE name = $1", [name])  // parameterized

# NoSQL Injection (MongoDB)
❌ db.users.find({ username: req.body.username })
   // attacker sends: { "$gt": "" } — returns all users
✅ Validate input type before passing to query
✅ Use schema validation (Zod, Joi) at API boundary

# Command Injection
❌ exec(`convert ${filename} output.jpg`)
✅ execFile('convert', [filename, 'output.jpg'])  // no shell interpolation

# Path Traversal
❌ readFile('./uploads/' + req.params.file)
✅ const safe = path.resolve('./uploads', req.params.file)
   if (!safe.startsWith(path.resolve('./uploads'))) throw new Error('Invalid path')

# General input validation rules:
- Validate at the API boundary (before any processing)
- Reject unknown fields (strict mode in Zod/class-validator)
- Limit string lengths and array sizes
- Validate content types match declared Content-Type header

Keep your own version of these notes — editable, searchable, and organised by your stack.

Start free