APIs are the backbone of modern applications, but they are also the most exposed attack surface. Unlike web pages, APIs rarely have the benefit of a browser's built-in security protections. This lesson covers how to test REST, GraphQL, and gRPC APIs for common and subtle security flaws.
API reconnaissance
Before testing, understand the API surface:
Documentation sources
| Source | What you learn |
|---|---|
OpenAPI/Swagger spec (/api-docs, /swagger.json) | All endpoints, parameters, schemas |
GraphQL introspection ({ __schema { types { name } } }) | All types, queries, mutations |
| gRPC reflection | All services and methods |
| Client-side JavaScript | API calls, endpoint URLs, auth patterns |
| Mobile app traffic (via proxy) | Endpoints the web client does not use |
| Error messages | Internal endpoint names, expected parameters |
Enumerate hidden endpoints
APIs often have endpoints not documented in the public spec:
# Common hidden endpoints
/api/debug
/api/internal
/api/admin
/api/v1 (old version still active)
/api/health
/api/metrics
/api/graphql (even if not advertised)
/actuator (Spring Boot)
/.well-known/openid-configuration
Use wordlists and tools like ffuf for directory brute-forcing:
ffuf -u https://api.example.com/api/FUZZ \
-w /usr/share/wordlists/api-endpoints.txt \
-mc 200,201,301,302,401,403
A 401 or 403 means the endpoint exists but requires auth. That is valuable information.
REST API testing
Authentication bypass
Test every endpoint with and without authentication:
# With auth
curl -H "Authorization: Bearer $TOKEN" https://api.example.com/api/users/me
→ 200 OK
# Without auth
curl https://api.example.com/api/users/me
→ Should be 401, not 200
Test with expired, malformed, and forged tokens:
# Expired token
curl -H "Authorization: Bearer $EXPIRED_TOKEN" https://api.example.com/api/users/me
# Malformed token (truncated)
curl -H "Authorization: Bearer eyJhbGciOiJ" https://api.example.com/api/users/me
# Token with "none" algorithm (JWT confusion)
# Header: {"alg": "none", "typ": "JWT"}
# Payload modified, no signature
curl -H "Authorization: Bearer eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJ1c2VyIjoiYWRtaW4ifQ." \
https://api.example.com/api/users/me
Broken Object-Level Authorisation (BOLA)
The #1 API vulnerability. For every endpoint that takes an ID:
# Get your resource
GET /api/users/101/documents/5001
Authorization: Bearer $USER_A_TOKEN
→ 200 OK
# Get someone else's resource
GET /api/users/102/documents/5002
Authorization: Bearer $USER_A_TOKEN
→ Should be 403, not 200
Test with:
- Sequential IDs:
5001,5002,5003 - UUIDs from other users' responses
- The IDs from response bodies (they often leak other users' resource IDs)
Broken Function-Level Authorisation
Access admin functions as a regular user:
# Regular user tries admin endpoint
POST /api/admin/users
Authorization: Bearer $REGULAR_USER_TOKEN
{"username": "newadmin", "role": "admin"}
# Regular user tries to change their own role
PATCH /api/users/me
Authorization: Bearer $REGULAR_USER_TOKEN
{"role": "admin"}
# Regular user tries HTTP method switching
DELETE /api/users/102
Authorization: Bearer $REGULAR_USER_TOKEN
Mass assignment
Send extra fields in request bodies:
# Normal update
PATCH /api/users/me
{"name": "Alice"}
# Mass assignment attempt
PATCH /api/users/me
{"name": "Alice", "role": "admin", "isVerified": true, "balance": 999999}
Check the response — did any of the extra fields update?
Rate limiting
Test rate limits on sensitive endpoints:
# Test login rate limiting
for i in $(seq 1 100); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST https://api.example.com/api/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"wrong"}')
echo "Attempt $i: $STATUS"
done
# Should see 429 after 5-10 attempts
Also test:
- Password reset endpoint
- API key generation
- Any endpoint that sends emails or SMS (cost abuse)
Excessive data exposure
Compare the API response with what the UI actually shows:
GET /api/users/me
{
"id": 101,
"name": "Alice",
"email": "[email protected]",
"passwordHash": "$2b$10$...", ← Should not be here
"internalId": "usr_a1b2c3", ← Should not be here
"stripeCustomerId": "cus_abc123", ← Should not be here
"role": "user",
"isAdmin": false, ← Reveals admin flag existence
"createdAt": "2025-01-01T00:00:00Z"
}
Check every response for fields that are:
- Internal identifiers
- Password hashes or credentials
- Third-party service IDs
- Flags that reveal information about permissions
Input validation
Test boundary conditions for every parameter:
# String fields
{"name": ""} # Empty string
{"name": "A".repeat(1000000)} # Extremely long string
{"name": "<script>alert(1)</script>"} # XSS payload
{"name": "Robert'); DROP TABLE users;--"} # SQL injection
# Numeric fields
{"amount": -1} # Negative number
{"amount": 0} # Zero
{"amount": 99999999999} # Integer overflow
{"amount": 0.001} # Tiny decimal
{"amount": "not_a_number"} # Wrong type
# Array fields
{"ids": []} # Empty array
{"ids": [1,2,3,...,10000]} # Huge array
{"ids": [1, "a", null, true]} # Mixed types
GraphQL API testing
Introspection
If introspection is enabled, you can map the entire schema:
{
__schema {
queryType { name }
mutationType { name }
types {
name
fields {
name
args { name type { name } }
type { name kind }
}
}
}
}
Introspection should be disabled in production. If it is enabled, that is a finding.
Query depth and complexity attacks
GraphQL allows nested queries that can exhaust server resources:
# Deeply nested query (DoS)
{
user(id: 1) {
friends {
friends {
friends {
friends {
friends {
name
}
}
}
}
}
}
}
Test if the server enforces:
- Maximum query depth
- Query complexity limits
- Timeout on expensive queries
Batch query attacks
Some GraphQL servers allow batched queries:
[
{"query": "mutation { login(email: \"[email protected]\", password: \"pass1\") { token } }"},
{"query": "mutation { login(email: \"[email protected]\", password: \"pass2\") { token } }"},
{"query": "mutation { login(email: \"[email protected]\", password: \"pass3\") { token } }"}
]
A single HTTP request attempts 3 logins. If the server does not limit batch size, rate limiting per request is bypassed.
Authorisation in GraphQL
GraphQL resolvers may not check authorisation consistently:
# Direct query (might check auth)
{ user(id: 102) { email, address } }
# Via relationship (might NOT check auth)
{ order(id: 5001) { user { email, address, paymentMethods { last4 } } } }
Test authorisation through different query paths. A resolver might be secure when accessed directly but leak data when accessed through a relationship.
Field suggestions
Some GraphQL servers suggest valid field names when you mistype:
{ user(id: 1) { passwrd } }
# Response: "Did you mean 'password' or 'passwordHash'?"
This reveals field names even when introspection is disabled.
gRPC testing
Tools
- grpcurl — command-line gRPC client (like curl for gRPC)
- grpcui — web-based GUI for gRPC services
- Postman — supports gRPC
Service enumeration
# List services (if reflection is enabled)
grpcurl -plaintext localhost:50051 list
# Describe a service
grpcurl -plaintext localhost:50051 describe myapp.UserService
Testing gRPC endpoints
# Call a method
grpcurl -plaintext \
-d '{"user_id": 101}' \
localhost:50051 myapp.UserService/GetUser
# Test BOLA — try another user's ID
grpcurl -plaintext \
-d '{"user_id": 102}' \
-H "authorization: Bearer $USER_A_TOKEN" \
localhost:50051 myapp.UserService/GetUser
gRPC-specific issues
- Reflection enabled in production — exposes all services and methods
- No TLS — gRPC often runs on internal networks without TLS
- Protobuf schema assumptions — the server may accept fields not defined in the published proto file
- Streaming abuse — client streaming can be used to send massive amounts of data
API testing methodology
A systematic approach for API security testing:
1. Discover → Map all endpoints, methods, parameters
2. Enumerate → Find hidden endpoints, old versions, debug routes
3. Authenticate → Test auth bypass, token manipulation, session handling
4. Authorise → BOLA, function-level access, mass assignment on every endpoint
5. Inject → SQLi, NoSQLi, command injection, SSRF on every input
6. Abuse → Rate limiting, resource exhaustion, business logic
7. Inspect → Excessive data exposure in responses
8. Automate → Run OWASP ZAP, Nuclei, or custom scripts for regression
9. Report → Document findings with reproduction steps and impact
Summary
API security testing starts with reconnaissance — discover every endpoint, including undocumented ones. Test authentication (bypass, token manipulation), authorisation (BOLA on every endpoint that takes an ID, function-level access), input validation (boundary conditions, injection), and data exposure (check responses for leaked fields). For GraphQL, test introspection, query depth, batch attacks, and resolver-level authorisation. For gRPC, test reflection, TLS, and streaming abuse. Automate regression tests for known issues and test systematically using the OWASP API Security Top 10 as a guide.
