API Design

Security · Lesson 01

API Security Threats — the full landscape

Before you can defend an API you need a map of what can go wrong. This lesson builds that map: the three properties every attacker tries to destroy, the ten most-exploited API weakness classes, and the layered model that keeps attackers from a clean shot.

⏱ 14 min Difficulty: core Prereq: HTTP basics

By the end you'll be able to

Start with what you are protecting

Security is not a feature you bolt on at the end — it is a property you either designed in or accidentally designed out. Before picking any control, ask: what are you protecting, and from whom?

The classical answer is the CIA triad — three properties that, if any one is violated, you have a security incident:

PropertyWhat it means for an APIThreat that breaks it
ConfidentialityData is visible only to authorised partiesEavesdropping, data leaks, over-broad responses
IntegrityData is not tampered with in transit or at restMan-in-the-middle modification, injection attacks
AvailabilityThe API responds to legitimate requestsDDoS, resource exhaustion, lack of rate limiting

Think of your API as a delivery van carrying parcels. Confidentiality means the parcels stay sealed (no peeking). Integrity means the contents cannot be swapped during transit (the seals are tamper-evident). Availability means the van arrives on time (it is not blocked at a checkpoint or run off the road). An attacker only needs to break one of the three.

Threat modelling in four questions

A threat model is a structured way to answer: "what could go wrong?" before you build, not after you're breached. You don't need a formal notation; four questions get you 80% of the way:

  1. What are you building? — list the API endpoints and the data they touch.
  2. What can go wrong? — walk through each endpoint: what if the caller is not who they say, or sends a crafted input?
  3. What will you do about it? — pick a countermeasure for each threat.
  4. Did it work? — test your controls; repeat.

The OWASP API Security Top 10

OWASP (the Open Web Application Security Project) publishes a ranked list of the most-exploited API weaknesses, updated periodically. The 2023 edition is the current reference. Each category represents a real pattern seen across real breaches — these are not academic edge-cases.

#NameOne-line summary
API1Broken Object-Level Authorization (BOLA)Accessing another user's resource by changing an ID in the URL — the most common API flaw.
API2Broken AuthenticationWeak tokens, missing expiry, passwords accepted in query strings.
API3Broken Object Property-Level AuthorizationEndpoint returns (or accepts) fields the caller should never see or set.
API4Unrestricted Resource ConsumptionNo rate limits, no pagination caps — one caller can exhaust CPU, DB, or bandwidth.
API5Broken Function-Level AuthorizationAdmin-only endpoints are accessible to normal users because the check is missing, not hidden.
API6Unrestricted Access to Sensitive Business FlowsBots can abuse legitimate features (mass account creation, bulk coupon redemption).
API7Server-Side Request Forgery (SSRF)API fetches a URL supplied by the client, letting attackers probe internal services.
API8Security MisconfigurationOpen S3 buckets, debug mode in prod, missing CORS policy, default credentials left in place.
API9Improper Inventory ManagementOld API versions, shadow endpoints, undocumented endpoints — attackers find what you forgot you had.
API10Unsafe Consumption of APIsTrusting third-party API responses without validation — you inherit their vulnerabilities.

Notice that seven of the ten are authorisation and access-control failures — not cryptography, not exotic exploits. The most exploited API bugs are "can this caller do this thing?" checks that are missing or wrong.

⚠️ API3 — Mass Assignment (Broken Object Property Level Authorization): attack scenario

A user updates their profile via PUT /users/42. While exploring the API, the attacker issues a normal GET /users/42 and spots a "role": "user" field in the response. They craft a write request that adds that field back with an elevated value. The server uses an ORM method like Rails's .update(params) or a generic Object.assign(user, req.body) in Node — it blindly maps every field in the request body to the model. The attacker's role: admin is silently persisted, granting full administrative access without any privilege check ever firing.

# Attacker discovers role field via a normal GET response, then writes it back elevated
PUT /users/42
Authorization: Bearer <valid_user_token>
Content-Type: application/json

{ "name": "Alice", "role": "admin", "verified": true }

# Vulnerable server — ORM blindly maps all fields:
HTTP/1.1 200 OK
{ "id": 42, "name": "Alice", "role": "admin", "verified": true }

# Fix — use an explicit allowlist; never mass-assign untrusted fields:
# allowed = ['name', 'email', 'avatar_url']
# user.update(pick(req.body, allowed))   ← role and verified are silently dropped
⚠️ API5 — Broken Function Level Authorization: attack scenario

Admin-only routes are often on a predictable path pattern (/admin/, /internal/, /manage/). Developers sometimes omit the authorization middleware because they assume "only admins know the URL." An attacker who is authenticated as a regular user simply guesses the path using common wordlists or by noticing admin-flavored links in JavaScript bundles. The route exists, the handler runs, and because no RBAC check was ever added, the full admin response is returned. Security by obscurity — hiding the URL — is not access control.

# Attacker is authenticated as a regular user, guesses the admin route
GET /api/v1/admin/users
Authorization: Bearer <regular_user_token>

# Vulnerable server — route exists but authorization middleware was never added:
HTTP/1.1 200 OK
[
  { "id": 1, "email": "alice@example.com", "role": "admin" },
  { "id": 2, "email": "bob@example.com", "role": "user" }
  // ... full user list returned to a regular user
]

# Secure server — RBAC checked on every route, never by URL convention alone:
HTTP/1.1 403 Forbidden
{ "error": "insufficient_scope" }

# Defense: enforce require_role('admin') middleware on every admin route explicitly.
# Never rely on URL obscurity — attackers enumerate paths with wordlists (ffuf, dirsearch).
⚠️ API6 — Unrestricted Access to Sensitive Business Flows: attack scenario

A legitimate POST /checkout/apply-coupon endpoint is designed for users to apply a promotional code at checkout. Because it is a public, functional feature, no bot protection is applied. An attacker scripts a loop that tries short, human-memorable codes (or a small published pattern space) at 10,000 requests per minute — a brute-force coupon stuffing attack. Valid codes return a discount; invalid ones return a 400. Weak codes get found quickly; the endpoint behaves exactly as designed — the problem is it was never protected against automated, high-volume use.

# Bot iterates possible coupon codes at scale — no rate limit stops it
POST /checkout/apply-coupon
{ "code": "SAVE50" }   # → 200 OK {"discount": "50%"}  ← valid code found

POST /checkout/apply-coupon
{ "code": "SAVE51" }   # → 400 Bad Request {"error": "invalid code"}

# Math: full 6-char base36 space = 36^6 ≈ 2.18e9 codes.
# At 10,000 req/min → ≈ 2.18e9 / 1e4 ≈ 2.18e5 min ≈ 151 days for full brute force.
# Real risk is shorter: weak patterns (SAVE##, SUMMER) collapse the space to minutes.
# Defense: rate limit per user session (e.g. 5 attempts/min), add CAPTCHA on
# repeated failures, use cryptographically random unguessable codes (≥128 bits),
# and alert on anomalous coupon-check volume from a single token or IP.
⚠️ API7 — Server-Side Request Forgery (SSRF): attack scenario

An API offers a "render preview" feature: the client supplies a URL and the server fetches it to generate a thumbnail or extract metadata. The URL is caller-controlled. An attacker supplies the AWS instance metadata endpoint (http://169.254.169.254/) — a non-routable IP that is reachable from the server's internal network but not from the public internet. The server dutifully fetches it and returns the response, which contains IAM credentials, instance identity documents, and internal network topology. No authentication is required to reach the metadata service — only being on the same internal network, which the server is. The attacker now has temporary AWS credentials and can move laterally through the cloud environment.

# Attacker supplies the AWS instance metadata URL as the preview target
POST /api/render-preview
Authorization: Bearer <valid_user_token>
Content-Type: application/json

{ "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/" }

# Vulnerable server fetches the internal URL and returns the body:
HTTP/1.1 200 OK
{
  "preview": "MyEC2InstanceRole"   // ← IAM role name leaked
}

# Follow-up — attacker fetches the credentials directly:
{ "url": "http://169.254.169.254/latest/meta-data/iam/security-credentials/MyEC2InstanceRole" }
# Response contains AccessKeyId, SecretAccessKey, Token — full AWS access

# Defense: validate/allowlist URLs before fetching (only allow specific trusted domains),
# block all RFC-1918 and link-local ranges (10.x, 172.16-31.x, 192.168.x, 169.254.x)
# at the HTTP client level. Never pass raw user-supplied URLs to internal HTTP clients.
⚠️ API9 — Improper Inventory Management: attack scenario

A team migrates from v1 to v2 of their API, correctly adding authentication and rate limiting to all v2 routes. The v1 routes are left in place for backward compatibility during a transition period — but the security hardening was never retrofitted to them. An attacker enumerates common path prefixes (/api/v1/, /api/v2/, /internal/, /dev/, /staging/) using automated tools. The v1 endpoint responds and returns data without the auth checks present in v2. The attacker exploits the forgotten endpoint to access user records without a valid token — a vulnerability that does not exist anywhere in the v2 codebase and was never included in any security review.

# v1 endpoint — forgotten during migration, auth checks were never added
GET /api/v1/users/42
# No Authorization header required

HTTP/1.1 200 OK
{ "id": 42, "email": "alice@example.com", "phone": "+1-555-0100" }

# v2 endpoint — correctly secured, returns 401 without a token
GET /api/v2/users/42
HTTP/1.1 401 Unauthorized

# Attacker enumeration pattern — automated path guessing:
# ffuf -u https://api.example.com/api/FUZZ/users/1 -w versions.txt
# versions.txt: v1, v2, v3, internal, dev, staging, beta, legacy

# Defense: maintain an API gateway inventory of every route (including old versions).
# Decommission old routes explicitly — return 410 Gone, not 200.
# Apply the same auth/authz middleware to ALL versions of the same route.

BOLA up close — the #1 bug

Broken Object-Level Authorization is so common it earned the top spot. The pattern is simple: a caller fetches a resource by ID, and the server checks only that the caller is authenticated — not that the caller owns the resource.

# Attacker is user 99. They try user 42's invoice:
GET /v1/invoices/42
Authorization: Bearer <valid_token_for_user_99>

# Vulnerable server response — no ownership check:
HTTP/1.1 200 OK
{
  "id": 42,
  "owner_id": 501,
  "amount": "$9,200",
  "card_last4": "4242"
}

# Fix — check ownership server-side every time:
# SELECT * FROM invoices WHERE id=42 AND owner_id=99
# → 0 rows → 404 Not Found (or 403 Forbidden)

Defense in depth — the layered model

No single control is unbreakable. Defense in depth means stacking independent layers so that an attacker who defeats one layer still faces the next. The classic analogy is a medieval castle: a moat, then a wall, then a gatehouse, then inner guards — each layer buys time and raises cost for the attacker.

For an API the layers look like this (outer to inner):

Layer 4 — Network perimeter / WAF / DDoS protection Layer 3 — API Gateway · TLS termination · Rate limiting · Auth token validation Layer 2 — Application service · Input validation · Business-logic authz (BOLA checks) Data store Parameterised queries · Encryption at rest · Least-privilege DB user Audit logging · Secrets in vault, not in code Attacker
Each ring is an independent defense. An attacker must defeat all four rings to reach the data. Defeating the WAF does not grant database access.
🎯 Interview angle

When asked "how would you secure this API?" start with a threat model, not a list of tools. Say: "First I'd identify what data is at stake and who the callers are. Then I'd walk through authorization checks at every endpoint, validate all inputs, enforce TLS, and add rate limits." That structure shows you think in layers, not in a product catalogue.

⚠️ Common trap

Treating authentication as sufficient for authorization. A caller with a valid token is authenticated — you know who they are. That says nothing about what they're allowed to do. Most OWASP Top 10 entries are authorization failures, not authentication failures. Always ask both: "Is this caller who they say?" and "Are they allowed to do this to this resource?"

✅ Do this, not that

Do build a short threat model (even a notes file) before writing the first endpoint. Don't wait until a security review or a production incident. BOLA bugs are trivially easy to prevent at design time — they are expensive to fix after the schema and the clients have frozen.

Under the hood: how it actually works

A BOLA attack runs in three phases: enumerate (guess or discover valid resource IDs), access (call the endpoint with a different user's ID using your own valid token), and exfiltrate (read the response and repeat). No special tooling is required — a loop in a shell script is enough.

# ── Phase 1: Attacker authenticates as their own account (user_99) ──────────
POST /v1/auth/token
{ "username": "attacker@example.com", "password": "hunter2" }

HTTP/1.1 200 OK
{ "token": "eyJ...", "user_id": 99 }

# ── Phase 2: Attacker views their own order to learn the URL pattern ─────────
GET /v1/orders/123
Authorization: Bearer eyJ...

HTTP/1.1 200 OK
{
  "id": 123,
  "owner_id": 99,
  "total": "$42.00"
}

# ── Phase 3: Attacker increments the ID — vulnerable server has no ownership check
GET /v1/orders/124
Authorization: Bearer eyJ...

# Vulnerable server — returns another user's order with PII:
HTTP/1.1 200 OK
{
  "id": 124,
  "owner_id": 501,
  "email": "victim@example.com",
  "address": "12 Maple St, Springfield",
  "card_last4": "7734"
}

# ── Phase 4: Attacker iterates 124 → 9999 in a loop to dump all orders ──────
# for id in $(seq 124 9999); do
#   curl -s -H "Authorization: Bearer eyJ..." /v1/orders/$id >> dump.json
# done

# ── Phase 5: Fixed server — ownership check blocks the request ───────────────
# Server SQL: SELECT * FROM orders WHERE id=124 AND owner_id=99 → 0 rows
HTTP/1.1 403 Forbidden
{ "error": "forbidden" }
Attack phase How it runs (on the wire) How to detect it
ID enumeration probe Attacker sends sequential requests to a resource endpoint (e.g. /orders/100, /orders/101 …) using their own valid token, watching for 200 vs 404/403 to map which IDs exist. Spike in requests to the same endpoint from one token within a short window; sequential numeric IDs in URL; high 404 rate from a single IP or token.
BOLA access A single authenticated request to a resource the token owner does not own; the server returns 200 with another user's data instead of 403/404. Response body contains a user_id / owner_id that does not match the token's subject claim; cross-user access logged at the application layer.
Bulk exfiltration Automated loop (shell, Python, Burp Intruder) iterates a large ID range; requests arrive faster than human browsing, often without think-time between calls. Request rate for a resource endpoint from one token exceeds a normal-use threshold (e.g. >30 req/min); anomaly detection on per-token resource access count; SIEM alert on sustained sequential ID pattern.

How to debug & inspect it

Finding BOLA in your own API does not require a scanner. Access logs already contain the evidence — you just need to know what to look for.

# Scan access logs for sequential ID probing on /orders by a single token $ grep 'GET /v1/orders/' access.log | grep ' 200 ' | awk '{print $7}' | sort | uniq -c | sort -rn | head -20 312 /v1/orders/4821 311 /v1/orders/4822 309 /v1/orders/4823 # Seeing the same IP or token racking up sequential IDs is a red flag # Count how many distinct resource IDs one token accessed in the last hour $ awk '/GET \/v1\/orders\// {match($0, /Bearer ([^ ]+)/, tok); match($0, /\/orders\/([0-9]+)/, rid); print tok[1], rid[1]}' access.log \ | sort | uniq | awk '{print $1}' | sort | uniq -c | sort -rn | head -10 4876 eyJ0eXAi... ← one token accessed 4876 unique order IDs 3 eyJ0eXAj... ← normal user: 3 orders # Any token with a per-hour resource-ID count far above your median is suspicious
Symptom Cause Fix
Spike in 403s on /resource/{id} from one IP BOLA probe — attacker is hitting IDs they do not own; the server is correctly returning 403 but the probe is still happening. Block / rate-limit the IP; audit which IDs it accessed; verify none returned 200.
Unusually high per-user request count for a resource endpoint Bulk enumeration — attacker iterating IDs in a loop, possibly at high speed. Rate limit per-token per-endpoint; alert on more than N requests to resource/{id} within 1 minute.
A user ID appears in responses that does not match the authenticated user Missing per-object ownership check — the server verified the token but not that the caller owns the fetched record. Add WHERE owner_id = current_user to every query that fetches a resource by ID; never rely on client-supplied owner hints.
Sequential numeric IDs in the URL Predictable IDs make BOLA trivial to exploit — an attacker can enumerate the entire ID space mechanically. Use UUIDs or opaque IDs; sequential IDs alone are not a vulnerability but they remove enumeration friction entirely.
⚠️ Audit checklist — find BOLA in your own API
  1. List every GET/PUT/PATCH/DELETE endpoint that accepts a resource ID (e.g. /orders/{id}, /users/{id}/profile). These are all candidates for BOLA.
  2. For each endpoint, find the database query it executes and check whether it filters on owner_id = current_user (or equivalent). A query like SELECT * FROM orders WHERE id = ? with no ownership predicate is vulnerable.
  3. Write a test with two user accounts: authenticate as user A, then call a resource that belongs to user B. The response must be 403 or 404 — if it is 200, you have BOLA.
  4. Check indirect object references too — not just top-level IDs. If your API exposes nested resources (/orders/{order_id}/items/{item_id}), verify that the item actually belongs to that order and that the order belongs to the caller.
  5. Review your logs for the patterns above: sequential-ID runs from a single token, cross-user owner_id in 200 responses, and anomalous per-token resource access counts.

Failure under attack (ops signals)

When not to apply this control naively

Why defense-in-depth has order

Authenticate identity, authorize action, validate input, limit rate, encrypt in transit and at rest, log for forensics. Reordering (e.g. validate after expensive work) creates DoS and bypass paths. Standout engineers map each control to a STRIDE threat it kills.

Failure under attack / misconfig

SignalLikely issueResponse
401/403 spike same key fingerprintStolen or stuffed credentialsRevoke, step-up, rate limit by IP+key
Sequential ID 404/200 patternIDOR / enumerationAuthz on object, opaque ids, rate limit
TLS handshake failures post-deployChain/SNI/cipher mismatchopenssl s_client, rollback cert

🧠 Quick check

1. An attacker intercepts API traffic and changes the JSON payload in a response before it reaches the client. Which CIA property did they violate?

Integrity means data cannot be altered in transit without detection. Swapping payload contents is a classic integrity attack. TLS prevents this — that is lesson 02.

2. A caller sends GET /orders/9999 using their own valid token, but order 9999 belongs to a different user and they receive it anyway. Which OWASP category is this?

The caller is authenticated (valid token) but the server failed to check that the caller owns order 9999. That is textbook BOLA — missing per-object ownership check.

3. Which statement best describes "defense in depth"?

Defense in depth means multiple independent barriers. No single layer is assumed perfect; each raises the attacker's cost and limits blast radius when one layer fails.

4. Which OWASP category covers an API endpoint that lets a bot create 10,000 fake accounts in minutes because there is no rate limit?

API6 covers abuse of legitimate business logic at scale. The endpoint works as designed — but without bot detection or rate limiting, automated abuse is trivial.

✍️ Exercise: threat-model a ride-share API (try before opening)

You are designing an API for a ride-share app. The key endpoints are: POST /rides (request a ride), GET /rides/{id} (track your ride), GET /drivers/{id}/location (see driver position). For each endpoint, name one threat and one countermeasure.

Model answer:

EndpointThreatCountermeasure
GET /rides/{id}BOLA — rider A reads rider B's tripServer-side: SELECT … WHERE id=X AND rider_id=current_user
POST /ridesBot spam — 1000 fake ride requests exhaust driver supplyRate limit per account + CAPTCHA on suspicious volume
GET /drivers/{id}/locationStalking — anyone can track any driverOnly expose location to the matched rider for the active trip; return coarse position otherwise

Rubric: ✓ identified the correct OWASP category for each threat ✓ countermeasure is applied server-side, not client-side ✓ answers are proportionate (coarse location for privacy, not just "add auth").

Key takeaways

Sources & further reading