API SECURITY
API security testing checklist
An API has no UI to hide behind. Every rule you enforce in the browser or the mobile app can be skipped by a caller sending raw requests, so an API has to defend itself on its own terms. This checklist walks the OWASP-aligned checks that find the most real, exploitable bugs — with example requests you can run against a system you are authorized to test.
1. Broken access control
This is number one on the OWASP list for a reason: it is the flaw attackers reach for first and the one that leaks the most data. The API returns objects based on an identifier in the request, but never checks whether the caller is allowed to see that object.
Horizontal: another user's data
Authenticate as a low-privilege user, request your own record, then increment the ID and replay with your own token:
# Your record — 200 expected
curl -H "Authorization: Bearer $TOKEN" \
https://api.example.com/v1/users/1001
# Someone else's — this MUST NOT return their data
curl -H "Authorization: Bearer $TOKEN" \
https://api.example.com/v1/users/1002
If the second call returns another user's data, that is broken object-level authorization (BOLA). Sequential integer IDs make it trivial to enumerate every record; UUIDs raise the effort but are not the fix — the fix is checking ownership on the server for every request.
Vertical: privileged actions
Take an admin-only route and call it with a regular user's token. Deletes, role changes, and
"list all users" endpoints are the usual suspects. A 200 where you expected
403 is the finding.
2. Authentication
Two failures dominate here. The first is endpoints that simply forgot to require
authentication — strip the Authorization header and replay; anything that still
returns data is exposed. The second is weak token handling:
- Expired tokens are actually rejected, not merely hidden by the client.
- Tokens are validated server-side — a JWT with
alg: noneor an unverified signature is a critical bug. - There is a rate limit on login and password-reset, so credentials cannot be brute-forced.
- Error messages do not reveal whether a username exists ("no such user" vs. "wrong password").
3. Injection
Injection happens whenever caller input reaches an interpreter — a SQL engine, a shell, an HTML renderer — without being safely separated from the code around it.
SQL injection
Send classic probes into any parameter that could reach a database and watch for the tell-tale signs:
GET /api/products?id=1'
GET /api/products?id=1 OR 1=1
GET /api/products?id=1; WAITFOR DELAY '0:0:5'--
A database error in the response, a set of records that should not match, or a five-second delay on the time-based payload each indicate the input is reaching SQL unescaped. Parameterized queries (prepared statements) are the fix — string concatenation is the cause.
Command injection
Any endpoint that touches the filesystem, generates a PDF, pings a host, or shells out is a candidate. Append shell metacharacters and look for command output in the response:
GET /api/export?file=report.csv;id
GET /api/ping?host=127.0.0.1|whoami
Reflected XSS
If an API echoes input into an HTML response (error pages and search endpoints are common), send a marker and check whether it comes back unescaped:
GET /api/search?q=<script>alert(1)</script>
The payload appearing verbatim in an text/html response means output encoding is
missing.
4. Security headers
Headers are the cheapest hardening you will ever ship, and their absence is one of the most common findings in any scan. Check the response for:
Strict-Transport-Security— forces HTTPS on every future request.X-Content-Type-Options: nosniff— stops MIME-type guessing.X-Frame-Optionsor aframe-ancestorsCSP — blocks clickjacking.Content-Security-Policy— the single most effective control against XSS.
We wrote a separate guide on what each header does and how to set it, because "add security headers" is easy advice and surprisingly easy to get subtly wrong.
5. CORS
A permissive CORS policy lets a malicious website make authenticated requests to your API using a logged-in victim's browser. Inspect the response to a cross-origin request:
curl -i -H "Origin: https://evil.example" \
https://api.example.com/v1/account
The dangerous combination is Access-Control-Allow-Origin reflecting any origin
you send together with Access-Control-Allow-Credentials: true. That pairing
lets any site read authenticated responses. Allow only the specific origins you control, and never
reflect the caller's origin blindly.
6. TLS and transport
- The API is reachable only over HTTPS; plain
http://either refuses or redirects. - The certificate is valid, unexpired, and covers the hostname in use.
- No sensitive data — tokens, passwords, PII — travels in URL query strings, where it is logged at every hop.
7. Information disclosure and rate limiting
Finally, check what the API gives away for free. Trigger an error deliberately (malformed JSON, a wrong type, a missing field) and read the response: stack traces, framework version banners, SQL fragments, and internal hostnames all hand an attacker a map. Errors should be generic to the caller and detailed only in your server logs.
Then fire a burst of requests at an expensive endpoint and confirm the server starts returning
429 Too Many Requests. No rate limit means credential stuffing, scraping, and
denial-of-wallet are all on the table.
Turning a checklist into a repeatable process
Running these once before launch is good. The real value is running them on every release and tracking what changed — which findings you fixed, which are still open, and whether a new endpoint quietly reintroduced an old class of bug. A checklist in a document does not do that; it just gets stale.