API Security Patterns — Deep Dive
Level: Intermediate
Pre-reading: 08 · Security Summary · 08.01 · OAuth2 & OIDC
Defense in Depth for APIs
API security is not a single control — it's layered defenses. An attacker who bypasses one layer hits the next.
graph TD
Internet --> WAF[WAF - OWASP rules · DDoS protection]
WAF --> GW[API Gateway - Auth · Rate limit · TLS termination]
GW --> Svc[Service - Input validation · AuthZ · Business logic]
Svc --> DB[Database - Least privilege · Parameterized queries]
Transport Security
HTTPS and HSTS
| Control | What It Does | How to Implement |
|---|---|---|
| HTTPS everywhere | Encrypts traffic in transit; prevents eavesdropping | TLS 1.2 minimum; prefer TLS 1.3 |
| HTTP → HTTPS redirect | Forces upgrade from plaintext | 301 redirect + HSTS |
| HSTS header | Browser never connects over HTTP again | Strict-Transport-Security: max-age=31536000; includeSubDomains |
| Certificate pinning | Mobile apps reject unexpected certs | Use with caution — hard to rotate |
TLS 1.3 is significantly more secure than 1.2
TLS 1.3 removes weak cipher suites, requires forward secrecy, and has a faster handshake. Disable TLS 1.0 and 1.1 at the load balancer.
Input Validation and Sanitization
Every trust boundary is a validation boundary. Validate at the edge (gateway) and again in each service.
| Input Type | Attack | Defense |
|---|---|---|
| SQL parameters | SQL Injection | Parameterized queries / ORM; never string concat |
| HTML/JS fields | XSS | Encode output; use CSP headers; strip HTML on input |
| File uploads | Path traversal, malware | Validate extension + MIME type; scan content; store outside web root |
| JSON/XML payload | XXE, billion laughs | Disable external entity processing; set payload size limits |
| URL parameters | Open redirect | Validate redirect targets against allowlist |
| Headers | Header injection | Sanitize user-controlled header values |
// WRONG — SQL injection vulnerability
String query = "SELECT * FROM users WHERE id = " + userId;
// CORRECT — parameterized query
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
stmt.setString(1, userId);
CORS — Cross-Origin Resource Sharing
Browsers enforce Same-Origin Policy — JavaScript on app.example.com cannot call api.example.com unless the API explicitly allows it via CORS headers.
| CORS Header | Purpose | Safe Value |
|---|---|---|
Access-Control-Allow-Origin |
Which origins can call this API | Explicit allowlist: https://app.example.com |
Access-Control-Allow-Methods |
Allowed HTTP methods | GET, POST, PUT, DELETE — only what's needed |
Access-Control-Allow-Headers |
Allowed request headers | Content-Type, Authorization |
Access-Control-Allow-Credentials |
Allow cookies/auth headers | true — only if needed AND origin is not * |
Access-Control-Max-Age |
Cache preflight response | 600 (10 minutes) |
Never use Access-Control-Allow-Origin: * with credentials
* + credentials: true is rejected by browsers and for good reason. An allowlist-only approach prevents any origin from calling your credentialed endpoints.
Rate Limiting
Rate limiting prevents abuse, DDoS, brute force attacks, and accidental overload.
Rate Limiting Strategies
| Strategy | Description | Use Case |
|---|---|---|
| Fixed window | N requests per window (e.g., 100 req/min) | Simple; susceptible to burst at window edge |
| Sliding window | Smooth window — counts requests in rolling timeframe | More accurate; higher memory cost |
| Token bucket | Tokens refill at rate R; burst up to bucket size | Allows controlled bursts; standard approach |
| Leaky bucket | Requests processed at fixed rate; excess queued/dropped | Smooth output rate; no bursts |
Rate Limiting Dimensions
| Dimension | Example | Notes |
|---|---|---|
| Per IP | 100 req/min per IP | Easy to bypass with distributed bots |
| Per API key | 1000 req/min per client | Good for SaaS; ties to account |
| Per user | 50 req/min per authenticated user | Requires auth; most precise |
| Per endpoint | /login limited to 5 req/min |
Brute force protection |
| Global | 10,000 req/sec total | Platform-level protection |
graph LR
Req[Incoming Request] --> RL[Rate Limiter - Token Bucket]
RL -->|Tokens available| API[API Handler]
RL -->|No tokens| Rej[429 Too Many Requests]
API --> Resp[Response]
Authentication and Authorization at the API Layer
Authentication Patterns
| Pattern | Where Validated | Notes |
|---|---|---|
| JWT Bearer | Each service validates locally | Stateless; no DB lookup needed |
| Opaque token (API key) | Introspection endpoint or cache | Simpler for external developers; revocable |
| Session cookie | Server-side session store | Stateful; use for browser-facing apps |
| mTLS | Transport layer | No application code needed; strongest for service-to-service |
Authorization Patterns
| Pattern | Description | Best For |
|---|---|---|
| RBAC (Role-Based) | Permissions assigned to roles; users get roles | Most apps; straightforward to implement |
| ABAC (Attribute-Based) | Policy evaluates attributes (user, resource, env) | Fine-grained access; complex policies |
| ReBAC (Relation-Based) | Access based on relationships (Google Zanzibar model) | Social, document sharing |
| OAuth2 Scopes | Token carries allowed operations | API-level coarse-grained authorization |
OWASP API Security Top 10 — Detailed
| # | Risk | Attack Example | Mitigation |
|---|---|---|---|
| 1 BOLA | Broken Object Level Auth | GET /orders/456 — user can see other's orders |
Validate that the requesting user owns or has access to object 456 |
| 2 Broken Auth | Weak token validation | Expired JWT accepted; alg: none accepted |
Validate all JWT claims; pin algorithm |
| 3 BOPLA | Broken Object Property Level Auth | PATCH /user accepts {"role": "admin"} |
Use allowlist of updatable fields; never mass-assign |
| 4 Unrestricted Resource Consumption | No rate limits | Attacker calls /export 10,000 times |
Rate limit + payload size limits + timeout |
| 5 Broken Function Level Auth | User calls DELETE /admin/users/123 |
Validate role/permission on every endpoint; not just UI | |
| 6 Unrestricted Access to Sensitive Business Flows | Bulk coupon redemption bot | Business-level rate limiting; CAPTCHA; anomaly detection | |
| 7 SSRF | POST /fetch?url=http://169.254.169.254/ |
Validate/allowlist URLs; block internal IP ranges | |
| 8 Security Misconfiguration | Default creds, debug endpoints exposed | Automated config scanning; disable debug in prod | |
| 9 Improper Asset Management | Old API version /v1/ without auth |
Version sunset policy; API inventory | |
| 10 Unsafe API Consumption | Trusting external API response without validation | Validate and sanitize all external responses |
Security Headers — Quick Reference
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Content-Security-Policy: default-src 'self'; script-src 'self'
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(), microphone=()
Error Handling — Don't Leak Information
| ❌ Bad Response | ✅ Good Response |
|---|---|
SQLException: Table 'users' doesn't exist |
Internal server error. Reference: ERR-1042 |
User not found: user@example.com |
Invalid credentials (don't confirm email exists) |
Access denied for role 'VIEWER' on resource 'admin' |
403 Forbidden |
| Full stack trace in response body | Log internally; return generic error to client |
Stack traces in API responses are a reconnaissance gift
They reveal your technology stack, internal class names, file paths, and logic. Always log details server-side and return only a reference ID to the client.
What is BOLA and why is it the #1 API risk?
BOLA (Broken Object Level Authorization) means your API does not verify that the caller owns or has access to the specific object they're requesting. Example: GET /invoice/9999 — if your app only checks "is the user logged in?" but not "does user A own invoice 9999?", any authenticated user can access any invoice. It's #1 because it's easy to miss — especially in auto-generated CRUD APIs — and trivially easy to exploit.
What is the difference between rate limiting and throttling?
Rate limiting caps the number of requests a client can make in a time window (hard stop — 429 after limit). Throttling slows requests down or queues them rather than rejecting them. Rate limiting protects against abuse and overload. Throttling ensures fair resource distribution without hard rejection. Use rate limiting at the API gateway and throttling inside services.