API Security Testing

By Davy Rogers

APIs are your most exposed surface, and they skip the browser's safety nets. Test them hard.

APIs are where the testing payoff is highest, for two reasons. They're often your most exposed surface, and they don't get the browser's built-in protections that a rendered page does, no same-origin policy doing quiet work in the background. An API does exactly what the request tells it to. So you test them by sending the requests a well-behaved client never would.

Find every endpoint, not just the documented ones

Start by building the most complete map you can, because you can't test what you haven't found:

Documented sources: an OpenAPI or Swagger spec, GraphQL introspection, gRPC reflection, the client's JavaScript, captured mobile traffic.

Undocumented endpoints, which you fuzz for:

ffuf -u https://api.example.com/api/FUZZ -w api-endpoints.txt -mc 200,401,403

A 401 or 403 is a positive result here: it means the endpoint exists and wants auth. Now you know it's there.

REST testing

Work through these systematically against every endpoint:

  • Auth bypass. Call each endpoint with no token at all and see what answers.
  • Broken Object Level Authorisation, the single most common API vulnerability. Use one user's token to request another user's object:
GET /api/users/102/documents/5002  # With user 101's token, should be 403 not 200
  • Function-level authorisation. As a regular user, try POST /api/admin/users.
  • Mass assignment. Send PATCH /api/users/me {"role": "admin"} and see if it sticks.
  • Rate limiting. Fire 100 requests. Does anything return a 429?
  • Excessive data exposure. Read the responses. Are they handing back passwordHash or internal IDs you never asked for?

GraphQL

GraphQL has its own attack surface, mostly stemming from its flexibility:

  • Introspection. { __schema { types { name } } } should be disabled in production. If it answers, you've got the whole schema.
  • Depth attacks. Deeply nested queries that exhaust the server.
  • Batching. Multiple mutations in one request can slip past rate limits that count requests.
  • Resolver authorisation. Reaching data through a relationship can bypass a check that only guarded the direct path.

gRPC

The reflection service, if it's on, lists the methods for you:

grpcurl -plaintext localhost:50051 list
grpcurl -d '{"user_id": 102}' -H "auth: $USER_A_TOKEN" localhost:50051 myapp.UserService/GetUser

Then test the same authorisation questions you'd ask of REST.

The discipline that ties it together: map everything including the undocumented, test auth bypass on each endpoint, run BOLA on every ID you can find, and check for mass assignment and over-exposed responses. The OWASP API Security Top 10 is a good structure to test against, so you're covering categories rather than guessing.