JustAppSec

API Security Testing

Testing REST, GraphQL, and gRPC endpoints for common and subtle flaws.

0:00

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

SourceWhat you learn
OpenAPI/Swagger spec (/api-docs, /swagger.json)All endpoints, parameters, schemas
GraphQL introspection ({ __schema { types { name } } })All types, queries, mutations
gRPC reflectionAll services and methods
Client-side JavaScriptAPI calls, endpoint URLs, auth patterns
Mobile app traffic (via proxy)Endpoints the web client does not use
Error messagesInternal 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.


This training content is AI-assisted and reviewed by our team, but issues may be missed and best practices evolve rapidly. Send corrections to [email protected].