You are about to ship a REST or JSON API. Before production, you need to know whether the server survives requests you did not intend: wrong tokens, another user’s object IDs, oversized payloads, repeated requests, and malformed JSON. API security testing means sending those requests on purpose and confirming the server rejects each one or limits the damage.
The failures worth testing are authorization gaps more than cryptographic ones. The OWASP API Security Top 10 ranks Broken Object Level Authorization (BOLA) as the number one risk. BOLA happens when an API treats an object ID as the only proof that the caller may access the object. Then any caller who can guess or collect an ID can read or modify another user’s data. The stakes are measurable. One 2023 industry analysis found that 82% of organizations reported an API security incident in the past year, with BOLA accounting for about 35% of reported vulnerabilities.
Before release, test six areas in staging: authentication, authorization, rate limiting, input validation, replay protection, and failure cases. Staging is a pre-production environment that mirrors production but holds test data. There you can trigger failures and inspect logs without exposing real users. The checklist at the end puts every test in a form you can run against staging or convert into automated tests in your CI/CD pipeline.
Authentication: verify who is calling
REST APIs are stateless, so each request must carry its own proof of identity. The server must verify that proof on every request. The tests must confirm that valid credentials work and invalid ones are rejected.
Start with token handling if you use OAuth 2.0 or JWT. A JWT (JSON Web Token) is a signed token that carries claims, statements about the caller such as who issued it, who it was issued for, and when it expires. The server must check the signature, issuer, audience, expiry, and scopes (the permissions the token grants) on every request. A token that verifies cryptographically is still invalid if it targets a different audience.
Keep access tokens short-lived. Give refresh tokens, the long-lived credentials the client exchanges for new access tokens, their own expiry. MojoAuth’s token lifetime policy is one example: 1 hour for access tokens, 30 days for refresh tokens, and about 30 seconds of allowed clock skew when validating expiry. Test that an expired token, a token signed with a different key, and a token with the wrong audience are all rejected with an authentication error.
Then test the signature algorithm. Some JWT libraries accept the alg: none header, which lets an attacker forge a token with no signature at all. Send a token with alg: none and confirm the server rejects it.
If you issue API keys, treat them as credentials with a job description. API key best practices call for scoping each key to one purpose, expiring it by default, and supporting immediate revocation. Test that a revoked key stops working immediately, that a read-only key cannot write, and that creating a key without an expiry is impossible or at least flagged. Never hardcode keys in client code.
Transport is part of authentication. Tokens and keys must travel over HTTPS, never in URL parameters. URL parameters land in logs, proxies, and browser history. Enforce TLS 1.2 or newer, redirect HTTP to HTTPS, and send HSTS (HTTP Strict Transport Security) headers so clients refuse plaintext connections.
If the API accepts passwords, test the account protections: rate limiting on the login endpoint, account lockout, and multi-factor authentication where the risk justifies it. Credential stuffing, replaying username and password pairs stolen from other breaches, is cheap to run. The login endpoint needs its own abuse controls.
Authorization: verify what the caller may do
Authentication proves who the caller is. Authorization decides what that caller may read or change. The server must make the authorization decision, because the client can be modified. Testing authorization means proving that a valid caller cannot exceed their own permissions.
Start with object-level authorization, the BOLA case. Create two test accounts, user A and user B. Confirm that user A cannot access user B’s resources by substituting B’s object IDs into the requests. Run this for every resource type, not just the one in the documentation. Include IDs from other tenants, deleted records, and objects the caller should not know exist. The server should return an authorization or not-found error. Either is acceptable, but it must not return the data.
Then test function-level authorization. If the API exposes an admin endpoint that deletes a user or changes a role, call it with a regular user’s token and confirm the server returns an authorization error. Keep a function-to-role matrix, a table stating which roles may call which endpoints, so the test has an authoritative answer to check against.
Object property-level authorization is the third gap. The server might correctly decide that user A may read an object, then return every field on it, including fields user A should not see, such as an internal cost, a password hash, or another user’s contact details. This failure is Broken Object Property Level Authorization: inadequate validation of access to specific object properties. Prevent it by returning only the properties the caller may see and by rejecting mass assignment, where request fields bind directly to object properties without filtering. Test by sending role: admin or a similar privileged field in a create or update request and confirming the server ignores or rejects it.
Sensitive flows deserve their own tests. Refunds, transfers, and password changes should have step sequencing and audit. Test whether a caller can skip a required step, repeat a step, or call the steps in the wrong order. Confirm the server rejects the invalid sequence.
Rate limiting and resource consumption
Rate limiting restricts how many requests a client can send in a given period. It is the main defense against abuse and denial of service. Without it, one caller can exhaust database connections, CPU, or a third-party quota. Apply limits per user, IP, and API key, enforce object size limits, and move intensive work to asynchronous jobs so a slow task does not tie up request workers.
Test the limits you configured. Send requests until the server returns 429, the HTTP status for too many requests. Confirm the response includes a Retry-After header that tells the client when to retry. Counts should reset on the schedule you set. The Unstructured API, as an example, resets counts each minute and applies limits per API key or user, per HTTP method, and per resource path.
Match the limit identity to your real clients. If the limit keys on IP alone, an office full of users behind one NAT gateway (a shared public IP) or a shared egress IP will hit the ceiling together and block each other. Limit by API key or authenticated user where possible. Test with several identities to confirm the limits are per identity rather than global.
Then test the less obvious resource consumers. An endpoint that accepts a list of IDs, a large JSON body, or a file upload can be a denial-of-service vector even at a sane request rate. Send oversized payloads and confirm the server rejects them before parsing fully. If an endpoint accepts a URL and fetches it, test server-side request forgery (SSRF). In an SSRF attack, the server follows the attacker-supplied URL, which can reach internal metadata endpoints such as 169.254.169.254, the cloud metadata service. Confirm the server validates the URL against an allowlist and refuses internal addresses.
Input validation before the handler runs
Input validation rejects malformed or malicious data before it reaches business logic. The failure mode to test is coverage: validation must cover path parameters, query strings, headers, nested objects, and array elements, not just the obvious top-level fields.
Use allowlisting, where the server accepts only values that match a known pattern, rather than maintaining a blocklist of dangerous inputs. For JSON bodies, validate the structure against a schema such as JSON Schema, Joi, or Zod before the core handler runs.
Then send the classic payloads: SQL injection and command injection strings, and XML external entity (XXE) payloads, which trick an XML parser into reading local files or internal URLs, if any endpoint accepts XML. Malformed JSON should produce a client error, not a server error. A server error tells an attacker the parser crashed, and the response may leak internals.
If the API accepts file uploads, validation cannot stop at the file name. An attacker can name a file invoice.pdf and fill it with JavaScript or a shell script. Check the file’s magic bytes and MIME type: magic bytes are the identifying bytes at the start of the file that reveal its real format, and the MIME type is the declared content type. Confirm the server rejects files whose content does not match the declared type and stores uploads outside the directory the web server serves.
Validation belongs at every layer. The load balancer, API gateway, handler, and data store should each enforce the checks they can see. A gateway can reject oversized headers or malformed JSON early, while the handler must still validate semantic rules such as whether a date range is valid or an amount is positive. Test that a request that passes the outer layers but violates a business rule is rejected at the handler.
Replay protection and idempotency
A captured request, or a client retry after a timeout, can reach the server twice. For a read, the duplicate is harmless. For a payment, a transfer, or any operation with a side effect, the duplicate can execute twice. The server prevents this by storing a record of requests it has already processed and returning the stored outcome when the same request arrives again.
The common mechanism for unsafe methods is an idempotency key. The client generates a unique value, sends it in a header, and the server stores the key with the first response. When the same key arrives again, the server returns the stored response. Idempotency keys for unsafe methods are a standard part of API security checklists. Test that retrying the same request with the same key returns the same response, that a new key executes a new operation, and that the server handles a key reused after its retention window.
For token-based APIs, the JWT jti claim, a unique identifier for each token, serves the same purpose. The server uses jti to prevent token replay, so a token that was revoked or already used is not accepted a second time. Short-lived access tokens bound the replay window by construction. Refresh tokens need a stricter test: redeem a refresh token, then redeem it again with the same value and confirm the server rejects the second use and revokes the token family. A refresh token that never rotates becomes a durable foothold for an attacker who steals it.
Rate limiting does not stop replay. A rate limit stops a flood, but an attacker who replays one captured payment request inside the limit still triggers the duplicate. Idempotency keys and jti checks are the controls that prevent the second execution.
Failure cases to verify in staging
Staging is where these tests belong, because you can create test data, trigger failures, and inspect logs without touching production data. Several failure cases deserve explicit tests beyond the happy path.
Test unauthenticated, invalid, and expired token access to every endpoint. Confirm each is rejected with an authentication or authorization error. Then test the authenticated-but-wrong-scope case: a token that may read but not write must receive an authorization error on a write endpoint.
Test HTTP method handling. A resource endpoint that accepts GET and DELETE should reject PUT and POST with a method-not-allowed error, not silently run a default handler. Method testing catches endpoints that accept unintended verbs.
Test error handling for information leaks. A server error response that includes a stack trace, a database query, or an internal path gives an attacker a map of your stack. Confirm that error responses are generic and that detailed errors appear only in server logs. The same applies to validation errors: an endpoint that echoes the full submitted object back in an error message can leak fields you meant to filter.
NIST’s API guidance calls out shadow, zombie, and orphaned APIs: endpoints that are undocumented, forgotten, or decommissioned but still reachable, and that often lack security controls. Before release, run a discovery pass against staging to find every reachable path. Compare it to the documented surface, and remove or secure anything extra. Versioned endpoints need the same treatment: if /v1 is deprecated, confirm it is locked down or gone, not quietly serving old insecure behavior.
Finally, test business logic sequences, the cases black-box tools miss. Fuzz testing and sequence testing, where you call steps in unexpected orders, uncover bugs that individual request tests do not. Try confirming an order before creating it, applying a coupon twice, or completing a checkout with an empty cart. These are not injection attacks, but they are how attackers abuse the API’s actual behavior.
The pre-release checklist
Run these tests against staging before you cut the release. Each item names the request to send and the response that proves the control works.
Authentication
- Send no token, an invalid token, and an expired token to every endpoint. The server returns an authentication or authorization error for each.
- Send a JWT with
alg: none; the server rejects it. - Send a token with the wrong audience, and send a token signed with a different key. The server rejects both.
- Revoke an API key, then call with it; the server rejects the call, and creating a key without an expiry is impossible or flagged.
- Confirm tokens travel in headers, never in URL parameters, and that HTTP redirects to HTTPS.
Authorization
- As user A, access user B’s object by ID for every resource type. The server returns an authorization or not-found error.
- Call admin endpoints with a regular user’s token. The server returns an authorization error.
- Send
role: adminor a similar privileged field in an update; the server ignores or rejects it. - Read an object and confirm the response contains only the fields the caller may see.
Rate limiting and resource consumption
- Exhaust a rate limit. The server returns 429 with a Retry-After header, and counts reset per identity.
- Verify limits apply per user or API key, with each identity counted separately. A global counter or a shared office IP should not group all identities into one quota.
- Send an oversized JSON body and an oversized file; the server rejects them before full processing.
- If any endpoint accepts a URL, confirm the server blocks internal addresses and unreachable hosts.
Input validation
- Send SQL injection strings in query parameters, and send command injection in fields that reach the shell. The server rejects or neutralizes them.
- Send malformed JSON and out-of-schema bodies; the server returns a client error, not a server error.
- Upload a file whose content does not match its extension. The server rejects it by magic bytes or MIME type.
Replay protection
- Retry a create or payment request with the same idempotency key; the server returns the stored response and does not execute twice.
- Retry the same request with a new key; the server executes it once.
- Redeem a refresh token twice; the server rejects the second use and revokes the token family.
- Replay a request whose token carries a used
jti; the server rejects it.
Staging failure cases
- Every endpoint rejects requests without valid credentials, and rejects a valid token without the right scope.
- Unsupported HTTP methods return a method-not-allowed error, not a fallback handler response.
- Error responses contain no stack traces, SQL, or internal paths.
- Run a discovery pass and confirm that no reachable endpoint is undocumented or decommissioned.
- The API fails safely on business-logic sequences such as confirming before creating or applying a coupon twice.