API Security Testing: A Practitioner’s Methodology
APIs handle the real work behind modern apps: the login button on your banking app, the payment webhook on your e-commerce site, and everything in between. In 2024, over 60% of the data breaches OWASP tracked involved an API-layer flaw. Yet organisations routinely apply weaker testing practices to their APIs than to their web interfaces, partly because APIs are less visible and partly because testing them takes a different approach.
This guide covers how to assess API security the way an attacker would: methodically, with specific techniques, and with a focus on finding what automated scanners miss.
The OWASP API Security Top 10
Before testing anything, understand what you’re looking for. OWASP maintains a dedicated API Security Top 10, separate from the web application list, updated in 2023.
| Rank | Risk |
|---|---|
| API1 | Broken Object Level Authorisation (BOLA) |
| API2 | Broken Authentication |
| API3 | Broken Object Property Level Authorisation |
| API4 | Unrestricted Resource Consumption |
| API5 | Broken Function Level Authorisation |
| API6 | Unrestricted Access to Sensitive Business Flows |
| API7 | Server-Side Request Forgery (SSRF) |
| API8 | Security Misconfiguration |
| API9 | Improper Inventory Management |
| API10 | Unsafe Consumption of APIs |
This list tells you where APIs most commonly fail. Use it to structure your testing, not to limit it. The most damaging findings often sit between categories.
Phase 1: reconnaissance and API mapping
Before touching a single endpoint, map the complete API surface. The goal is to discover every endpoint the application calls, including those that do not appear in official documentation. Undocumented endpoints are where the most interesting findings live.
Extracting endpoints from JavaScript bundles
Modern single-page applications bundle their API routing into JavaScript files delivered to every browser. Attackers read these files. So should you.
# Extract API paths from a frontend JavaScript bundle
curl -s https://target.com/static/app.js | \
grep -oP '["'"'"'](/api/[^"'"'"'\s]+)["'"'"']' | sort -u
# Dedicated tool for larger bundles
python3 relative-url-extractor.py app.js
This often reveals internal endpoints, versioned API paths, and admin routes that were never intended to be publicly documented.
Finding API documentation
Even undocumented APIs often have documentation endpoints left accessible.
# Common documentation paths to check
curl https://target.com/api/docs
curl https://target.com/swagger.json
curl https://target.com/openapi.yaml
curl https://target.com/api-docs
curl https://target.com/v1/api-docs
curl https://target.com/.well-known/openapi
If documentation is accessible without authentication, you have a complete roadmap of the API. If it returns 404 while unauthenticated, try again with a valid session token, since the documentation may only be visible to logged-in users.
Monitoring mobile app traffic
Mobile apps communicate with backend APIs using the same infrastructure as web clients. Proxy the device through Burp Suite and use the application normally: make purchases, view profiles, change settings. The requests Burp intercepts reveal endpoints the web documentation never mentions.
Configure the mobile device to route traffic through your Burp listener, install the Burp CA certificate on the device, then interact with every feature of the app. Focus on high-value actions: payment flows, account settings, data exports, and admin-adjacent functionality.
Mapping documented vs. actual endpoints
The gap between what API documentation describes and what the application actually calls is where the most interesting findings live. Build a complete map of:
- All endpoints from official documentation
- All endpoints discovered in JavaScript bundles
- All endpoints captured during mobile app proxying
- Any endpoints discoverable through common path patterns (
/internal/,/debug/,/v2/,/admin/)
The intersection of “exists but undocumented” and “not protected like documented endpoints” is where vulnerabilities are found most frequently.
Phase 2: authentication testing
Authentication failures are high-severity findings with broad impact. Every authentication mechanism the API uses needs to be tested systematically.
API key testing
API keys are only as secure as how they are handled and validated.
# Keys should only appear in headers, not URLs
GET /api/data?api_key=sk_live_xxxxx # Bad — appears in logs and history
Authorization: Bearer sk_live_xxxxx # Correct placement
# Test with no key — should return 401
curl https://target.com/api/v1/users
# Test with an invalid key
curl -H "Authorization: Bearer invalid_key_xxxxxx" https://target.com/api/v1/users
# Test with a key from a deleted or suspended account
Also check whether API keys appear in JavaScript bundles, error messages, or Swagger documentation examples. This happens more often than it should, and when it does, every user of the application is exposed.
JWT vulnerabilities
JWTs have a documented history of implementation failures. Test all of them systematically.
The none algorithm attack:
import base64
import json
# Craft a JWT with alg: none — no signature required
header = base64.b64encode(
json.dumps({"alg": "none", "typ": "JWT"}).encode()
).decode().rstrip('=')
payload = base64.b64encode(
json.dumps({"sub": "admin_user_id", "role": "admin"}).encode()
).decode().rstrip('=')
forged_token = f"{header}.{payload}."
# If this token is accepted, the library accepts unsigned tokens
RS256 to HS256 algorithm confusion:
If the server uses RS256 (public/private key pair), the public key is often available at a JWKS endpoint. Some JWT libraries, when the token header specifies HS256 instead of RS256, will validate the signature using the public key as the HMAC secret. Since the public key is publicly available by design, an attacker can sign arbitrary tokens.
# Retrieve the public key
curl https://target.com/.well-known/jwks.json
# Use jwt_tool to generate an HS256-signed token using the RS256 public key
python3 jwt_tool.py [original_token] -X k -pk public.pem
Expiry and revocation testing:
Capture a valid JWT. Wait for it to expire. Then test whether the expired token is still accepted. Also test: can you use a JWT after explicitly logging out? Many applications never invalidate tokens. They only stop issuing new ones.
OAuth 2.0 testing
OAuth is complex enough that its failure modes are distinct and worth testing explicitly.
Key areas to test:
- State parameter: a missing or predictable
statevalue enables CSRF against the OAuth flow, letting an attacker link their account to a victim’s session - Redirect URI validation: overly permissive validation (accepting wildcard subdomains or path-only matching) allows stealing authorisation codes
- Scope enforcement: does the server enforce what scopes the token has, or does any valid token grant access to everything?
- Token leakage: authorisation codes appearing in server logs, Referer headers, or browser history
Phase 3: object-level authorisation testing
BOLA (Broken Object Level Authorisation) is consistently the most common and highest-impact finding in API security engagements. The concept is straightforward: test every endpoint that accepts an identifier and verify that your token cannot access data belonging to other users.
Setting up a BOLA test
Create two test accounts, User A and User B. Log in as both and document every object identifier tied to each account: order IDs, invoice numbers, profile IDs, document IDs, message IDs, and any other resource-specific identifiers.
Then, authenticated as User A, systematically request User B’s resources:
# User A's token attempting to access User B's order
curl -H "Authorization: Bearer [USER_A_TOKEN]" \
https://api.target.com/v1/orders/[USER_B_ORDER_ID]
# Expected: 403 Forbidden
# BOLA finding: 200 OK with User B's order data
Test variations beyond direct ID substitution:
- Numeric IDs: try sequential values, IDs from a different account’s timeframe
- UUIDs: they are not secret even if they appear random. They show up in URLs, emails, and shared links
- Unauthenticated requests: does the endpoint return data with no token at all?
- Different token types: can a customer-role token access supplier-role endpoints?
Indirect object references
BOLA is not always a direct ID in the URL path. Sometimes the vulnerable identifier is embedded in a request body, a query parameter, or referenced through another object.
POST /api/v1/messages/send
{
"to_user_id": 98765,
"message": "hello"
}
Test: can you send a message to a user who has blocked you? Can you enumerate user IDs by observing whether send succeeds or fails with different IDs? Does the response confirm whether a user ID exists or not, enabling enumeration?
Phase 4: function-level authorisation
Where BOLA tests access to another user’s data, function-level authorisation tests access to functionality reserved for a different role. The distinction matters because the fix is different: BOLA requires per-object ownership checks, function-level failures require per-endpoint role checks.
Testing role boundaries
# Can a regular user reach admin endpoints?
GET /api/v1/admin/users → 403 (expected)
POST /api/v1/admin/users → also 403? Test separately
DELETE /api/v1/admin/users/1 → also 403? HTTP methods often have different controls
# In a marketplace, can a customer reach supplier endpoints?
GET /api/v1/supplier/products → should be 403 for customer-role tokens
HTTP method and version variations
Authorisation is sometimes applied only to certain HTTP methods on a route. Test all methods on sensitive endpoints:
# These may have different access controls
GET /api/v1/users/profile
PUT /api/v1/users/profile
DELETE /api/v1/users/profile
PATCH /api/v1/users/profile
Also test API versioning. Older API versions (/v1/) are frequently less strictly controlled than current versions (/v2/). An admin endpoint that returns 403 in v2 may be fully open in v1.
Finally, test parameter manipulation: adding ?admin=true, &role=admin, or &bypass=1 to requests. This sounds naive, but it works on a surprising number of applications that check these parameters server-side without proper validation.
Phase 5: rate limiting and resource consumption
Missing rate limits have two distinct impacts: they enable credential stuffing on authentication endpoints, and they enable resource exhaustion on computationally expensive operations.
Testing authentication endpoints
# Rapid login attempts — adjust timing to avoid actual DoS impact
for i in $(seq 1 50); do
response=$(curl -s -o /dev/null -w "%{http_code}" \
-X POST https://api.target.com/auth/login \
-H "Content-Type: application/json" \
-d "{\"email\":\"victim@example.com\",\"password\":\"guess${i}\"}")
echo "Attempt ${i}: HTTP ${response}"
sleep 0.1
done
If all 50 attempts return without a 429 Too Many Requests response or an account lockout, that’s a finding. Rate limits should apply both per IP and per account, since rate limiting by IP alone is bypassable with proxy rotation.
Resource-intensive operations
Find endpoints that trigger expensive server-side operations:
- PDF generation
- Image processing or format conversion
- Email sending
- SMS OTP generation
- Large data exports
Test whether these can be called without restriction. A missing rate limit on an SMS OTP endpoint means an attacker can generate unlimited SMS charges against any phone number. A missing limit on PDF generation can exhaust server memory under load.
Pagination and data volume abuse
# Test for unrestricted page sizes
GET /api/v1/users?limit=1000
GET /api/v1/users?limit=99999
GET /api/v1/users?per_page=0
# Test for unbounded nested resource loading
GET /api/v1/orders?include=items,items.product,items.product.category
APIs that honour arbitrarily large limit or include parameters are vulnerable to resource exhaustion through authenticated requests, which can be used for denial-of-service from within the normal application flow.
Phase 6: GraphQL-specific testing
GraphQL has the same underlying vulnerabilities as REST (BOLA, broken authentication, insufficient rate limiting), plus some attack surfaces of its own worth testing explicitly. Don’t let the different transport layer lead to a less thorough assessment.
Introspection exposure
GraphQL’s introspection feature allows clients to query the complete API schema.
# Introspection query — should be disabled in production
curl -X POST https://api.target.com/graphql \
-H "Content-Type: application/json" \
-d '{"query": "{ __schema { types { name fields { name } } } }"}'
If introspection is enabled on a production endpoint, you get the complete type system: every query, mutation, type, and field the API supports. Use InQL or graphql-voyager to visualise the schema, and disable introspection in production.
Batch query attacks
GraphQL allows multiple operations in a single HTTP request. Rate limits applied per request do not protect against batching.
[
{"query": "mutation { login(email: \"victim@example.com\", password: \"pass1\") { token } }"},
{"query": "mutation { login(email: \"victim@example.com\", password: \"pass2\") { token } }"},
{"query": "mutation { login(email: \"victim@example.com\", password: \"pass3\") { token } }"}
]
A single request delivers three login attempts. At 100 objects per batch, this becomes 100 login attempts per rate-limited request. Verify whether per-operation rate limiting exists in addition to per-request limits.
Alias-based rate limit bypass
GraphQL aliases allow multiple queries of the same type in a single request using different names.
query {
a1: user(id: "victim1") { email phone }
a2: user(id: "victim2") { email phone }
a3: user(id: "victim3") { email phone }
}
Deep query attacks
GraphQL’s recursive data fetching allows nested queries that can exhaust server resources without explicit DoS intent.
{
user {
friends {
friends {
friends {
friends {
friends { id email phone }
}
}
}
}
}
}
Most GraphQL servers have no query depth limit by default. A single request like this can produce thousands of database queries. Verify that depth limits, complexity limits, and query cost analysis are configured.
Phase 7: business logic and data exposure
These findings require understanding how the application is supposed to work. Automated scanners cannot find them because they cannot model the application’s intended behaviour.
Excessive data exposure
APIs frequently return more data than the client needs.
{
"id": 12345,
"name": "John Smith",
"email": "john@example.com",
"phone": "+44 7700 900000",
"internal_risk_score": 847,
"account_flags": ["suspicious_activity_review"],
"stripe_customer_id": "cus_xxxxx",
"last_login_ip": "203.0.113.42"
}
The application may only display name and email in the UI, but if the API response includes internal_risk_score, account_flags, and stripe_customer_id, that data is accessible to anyone who can make the request. Define explicit response schemas. Return only what the client actually needs.
Business logic test cases
Test cases that require understanding the application’s intended behaviour:
- Negative quantities: can you add
-1of an item to a cart to trigger a negative total or credit? - Discount reuse: can the same discount code or referral credit be applied more than once?
- Workflow skipping: can you complete step 3 of a multi-step checkout without completing step 2?
- Race conditions: can you transfer more funds than your balance shows by submitting simultaneous transfer requests?
- Expired access: can you access resources after your subscription or trial period expired?
- Token reuse: can a single-use token (password reset, email verification) be used more than once?
None of these appear in any vulnerability scanner. Each requires knowing the system’s intended behaviour and testing every assumption about what should be impossible.
Common API security mistakes
A few patterns appear repeatedly across different engagements and codebases.
Trusting GUIDs as access control
UUIDs appear unpredictable but are not secret. They appear in URLs shared via email, logged by analytics systems, captured by browser extensions, and indexed by third-party tools. Never rely on unpredictable IDs as your sole authorisation mechanism. An ownership check must also be present.
Inconsistent security across API versions
An API’s v2 might have strong access controls while v1, still running and still functional, has none. Always test all accessible API versions. Legacy versions that were never officially deprecated are frequently accessible and frequently unprotected.
Hardcoded credentials in mobile apps
Mobile applications sometimes contain hardcoded API keys, internal endpoint URLs, or debug credentials embedded at build time. Decompile the APK with apktool or jadx and search for strings matching credential patterns.
Permissive CORS configuration
A wildcard CORS policy combined with cookie-based authentication means any malicious page the victim visits can make authenticated API requests on their behalf.
Access-Control-Allow-Origin: *
Access-Control-Allow-Credentials: true
These two headers together are a misconfiguration. Either restrict origins to specific trusted domains or avoid credentialed cross-origin requests.
Building secure APIs from the start
Finding API vulnerabilities in testing is valuable. Preventing them during development is better and significantly cheaper.
Centralise authorisation logic
Implement object-level ownership checks in a single middleware function and apply it consistently across all endpoints, instead of scattering checks across individual handlers where gaps are inevitable.
Use short-lived, scoped tokens
Issue JWTs with 15 to 60 minute expiry, specific aud and iss claims, and minimal scope. Store refresh tokens server-side so they can be individually revoked when needed.
Rate-limit everything
Apply limits per IP address and per authenticated user token. Use sliding window algorithms rather than fixed windows, since fixed window limits are bypassable by submitting requests at the window boundary.
Log API access with intent
Log not just the method and path but the authenticated user ID, the specific resource accessed, and the outcome. This makes it possible to reconstruct exactly what happened after a breach, and to spot BOLA through unusual access patterns before one is reported.
Define an explicit API inventory
Know every endpoint you are running. Endpoints you do not know about cannot be protected. Improper inventory management (API9) appears on the OWASP list because maintaining a complete, current API inventory is genuinely difficult at scale, but it is the prerequisite for everything else.
Need an API security test for your application? Get in touch to discuss scope. Most REST and GraphQL API assessments complete in 3 to 7 business days.