Why APIs need their own testing approach
APIs are different from traditional web pages. A web page has a user interface that hides some of the functionality, but an API exposes the functionality directly. The client can call any endpoint with any parameters. The server cannot rely on the UI to enforce rules. This is why API security testing is not just web testing with JSON.
This post is my personal study note on API security testing. It is a checklist I have built from engagements and from my own mistakes. The checklist covers authentication, authorization, input handling, rate limiting, documentation, and logging. I do not always follow it in this order, but I always try to cover every item before I finish.
Start by mapping the API
I cannot test an API that I have not mapped. The first step is collecting every endpoint that the application uses. I walk through the UI while capturing traffic, I read the JavaScript bundles for route definitions, and I look for OpenAPI or Swagger documentation. The documentation is a gift because it tells me the expected parameters and responses for every endpoint.
The map should include the HTTP method, the path, the parameters, the authentication requirement, and the response structure. I keep this map in a file that I can update as I discover new endpoints. The map is the foundation for every other test.
GraphQL APIs need a slightly different approach. The endpoint is often a single /graphql path, and the queries are sent in the request body. I use the introspection query to list the schema, and then I test the queries and mutations that the schema exposes. The schema tells me which fields contain sensitive data and which operations change state.
Authentication and token lifecycle
The first security layer I test is authentication. I look at how tokens are issued, how they are stored, and how they expire. I check whether a token can be used after it has been revoked, whether a refresh token can be reused, and whether a session can be replayed.
The login endpoint is the most obvious place to start. I test for username enumeration, brute force protection, and account lockout. I also test the password reset flow, because it often has its own authentication gaps. A reset token that is predictable, reusable, or sent to a client-controlled address is a serious finding.
Token storage matters too. If the client stores the token in localStorage, any XSS can read it. If the token is in a cookie, the cookie flags determine whether it can be read by scripts and whether it is sent over HTTPS. I do not judge the client storage by itself, but I note it because it changes the impact of other findings.
Authorization at the object level
The most common API security bug is a missing authorization check. I test every endpoint that accepts an identifier, even when the identifier looks random. I use two accounts and swap the identifiers to see whether the server returns data that belongs to the other account.
The check should be on the server. If the server trusts the account ID from the request body, an attacker can create resources for other users. If the server trusts the role from the payload, an attacker can become an admin. I test these fields by changing them and observing the response.
The HTTP status code is not enough. I compare the full response body and the side effects. A 403 with a partial response is still a leak. A 200 with a different user's data is a clear authorization failure.
Input validation and type confusion
API input validation is about more than SQL injection. The server receives structured data, and the structure itself can be the attack. I test every parameter with null values, empty values, negative numbers, very large numbers, and incorrect types. A parameter that accepts both a string and an object can behave differently depending on the type.
Type confusion becomes dangerous when the server uses the value in a security decision. For example, an API might accept {"role": "user"} and also {"role": ["admin"]}. The array form might pass a different validation path and grant a higher role. I test these variations systematically.
Extra fields in the request body are another issue. A deserializer might ignore unknown fields, or it might bind them to object properties. If I add a field like "isAdmin": true to the request, the server might accept it. Mass assignment bugs are common in APIs that bind the request body directly to a model.
Rate limiting and abuse
Rate limiting is not always a security control, but it is important for abuse protection. I test the login endpoint, the verification code endpoint, the search endpoint, and the bulk export endpoint. These endpoints are the most likely to be abused.
The limit should not be easy to bypass. A limit that tracks only the IP address can be bypassed with a proxy pool. A limit that tracks only the account can be bypassed by creating many accounts. A good limit combines the IP, the account, the device, and the action.
I also test whether the rate limit response reveals information. A response that says the account does not exist is different from a response that says the password is wrong. The difference can be used for enumeration.
Documentation and debug endpoints
Documentation endpoints are a double-edged sword. They make testing easier, but they also make attacking easier. I always look for OpenAPI pages, Swagger UI, GraphQL introspection, and debug routes. A debug endpoint that is accessible without authentication is a finding even if it only returns environment variables.
The documentation often reveals endpoints that are not used by the UI. I add every documented endpoint to my test matrix, including the ones that are marked as deprecated or internal. Deprecated endpoints are a favorite target because they may not receive the same security updates as the main endpoints.
I also look for hidden parameters in the documentation. An API might document a debug parameter, an internal flag, or a bypassCache option. These parameters can change the behavior of the endpoint and expose additional data.
Error handling and logging
The error handling in an API is a source of information leakage. I trigger errors by sending invalid data and read the response. A stack trace, a SQL fragment, an internal path, or a connection string in the error message is a finding.
The error format should be consistent. If the API returns different structures for different error types, the response can reveal whether a user exists, whether a token is valid, or whether an account is locked. I compare the responses and record the differences.
Logging is harder to test because I cannot see the server logs. I evaluate the logging design by reading the documentation and the configuration. The logs should record authentication failures, authorization denials, and suspicious request patterns without storing secrets. The logs should also be protected from tampering, because an attacker who can delete logs can hide the attack.
Business logic
The most interesting API bugs are often business logic bugs. I look for places where the client controls a value that should be calculated by the server. Prices, quantities, discounts, balances, and limits are common examples. If I can change the price to zero, increase the quantity beyond the limit, or reuse a coupon more than once, the API has a business logic flaw.
The order of operations matters too. I test whether an API validates the state of a resource before changing it. A payment API should not allow a refund on a payment that has already been refunded. An inventory API should not allow a negative stock level. These state-based bugs are hard to find with automated scanners.
I also test the same request twice. If the API allows the same transaction to be executed twice, the result may be a duplicate payment, a duplicate order, or a double-counted balance. Idempotency is an important design property, and its absence can be a security issue.
Pagination and filters
Pagination is a place where authorization bugs hide. A list endpoint may check the permissions for the first page and then return every page without a check. I test the pagination by requesting the last page and the largest page size.
The filters are another target. An API may use a filter parameter to restrict the data to the current user. If the filter is optional, I can remove it and see whether the API returns data from other users. I test the filter with different values and with no value.
The sort and search parameters can also reveal data. A search endpoint may include fields that are not shown in the normal response. I test the fields that the API accepts and look for unexpected data in the results.
The pagination and the filters are part of the API contract. The server should apply the authorization check to every item in the result set, not only to the first page.
Token and header abuse
The API may trust a header that the client can control. I test the common ones: X-Forwarded-For, X-Real-IP, X-User-Id, X-Role, and Authorization. A server that trusts the client for the user identity is vulnerable.
The X-Forwarded-For header is often used for rate limiting and logging. If the server trusts it, I can change the value to bypass a rate limit or hide the real source. I test the behavior with different header values.
The Authorization header is the standard place for the token, but some APIs also accept the token in a query string, a cookie, or a custom header. I test the alternative locations because the validation may be weaker.
The header abuse is a design issue. The server should derive the identity from the authenticated session, not from a client-controlled header.
API versioning
An API may have multiple versions running at the same time. The older versions often have weaker security controls. I test the API with different version paths, such as /v1, /v2, and /api/latest.
The versioning can also be controlled by a header or a query parameter. I test the common values and compare the responses. A deprecated version may return data that the current version hides.
The older version may not have the same authentication checks. I test the same endpoint in every version and compare the behavior. The difference is a finding.
The versioning should be part of the API inventory. I record the versions that exist and the differences between them.
Common mistakes I make
My biggest mistake is testing the API through the UI only. The UI hides endpoints, parameters, and behaviors. I need to capture the direct API traffic and test it independently. My second mistake is assuming that the documentation is complete. The real API may have more endpoints than the documentation shows.
The third mistake is forgetting to test the same endpoint with different roles and different accounts. The endpoint might work for me, but that does not mean it is secure for everyone. The fourth mistake is ignoring the response body and looking only at the status code.
My quick checklist
- Map every endpoint and parameter, including undocumented ones.
- Test authentication, token lifecycle, and account recovery.
- Swap identifiers across accounts and roles.
- Test input types, null values, extra fields, and mass assignment.
- Test rate limits across IPs, accounts, and actions.
- Inspect OpenAPI, GraphQL, debug, and deprecated endpoints.
- Read error messages and look for sensitive information.
- Test business logic, state transitions, and idempotency.
What I would do next time
Next time I want to build the API map before I write any test. The map is the single most important artifact, and it saves more time than any automation. I also want to spend more time on business logic, because that is where the impact is often the highest. API security is not a list of payloads. It is a conversation with the server, and the server should not be trusted just because it responds.