HackproofHacks
API Security 20 min read

The complete guide to API security testing.

APIs are the backbone of modern applications — and one of the most exploited attack surfaces. This guide walks through how to test an API the way an attacker would, covering reconnaissance, authentication, authorisation, business logic, and GraphQL.

Hassan Ansari

Hassan Ansari

A code card showing a broken-object-level-authorisation test against an API endpoint

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.

RankRisk
API1Broken Object Level Authorisation (BOLA)
API2Broken Authentication
API3Broken Object Property Level Authorisation
API4Unrestricted Resource Consumption
API5Broken Function Level Authorisation
API6Unrestricted Access to Sensitive Business Flows
API7Server-Side Request Forgery (SSRF)
API8Security Misconfiguration
API9Improper Inventory Management
API10Unsafe 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 state value 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 -1 of 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.

#API security #penetration testing #BOLA #OWASP API #GraphQL #JWT #authentication
Free newsletter

Liked this? I write one like it every week.

One practical security lesson in your inbox each week, explained the same simple way. Join 10,000+ readers. Unsubscribe anytime.

From the article

Need a security assessment?

HackproofHacks provides web application and API penetration testing — using the same techniques covered in this article, with your explicit authorisation.

Book a free scoping call

More on API Security.

All articles →
FAQ

Questions about this topic.

What is BOLA and why is it the most common API vulnerability?

BOLA stands for Broken Object Level Authorisation. It occurs when an API endpoint returns or modifies data based on an object identifier — like an invoice ID or user ID — without verifying that the requesting user actually owns that object. It's the most common API vulnerability because it's a logical failure, not a technical one. The application may have working authentication, but it's missing the ownership check on individual resources. An attacker doesn't need to break any encryption or bypass a login — they just change a number in the URL or request body.

How do attackers find API endpoints that aren't in the documentation?

Three main methods. First, parsing JavaScript bundles — most modern SPAs include hardcoded API paths in their frontend code, which is delivered to the browser and readable by anyone. Second, intercepting mobile app traffic with a tool like Burp Suite — mobile apps call the same backend APIs, and intercepting that traffic reveals endpoints the web documentation never mentions. Third, brute-forcing common path patterns — /v1/, /v2/, /internal/, /admin/, /debug/ — these often exist and sometimes lack proper access controls. Robots.txt, Wayback Machine snapshots, and old cached API documentation can also reveal decommissioned or internal-facing endpoints.

What is JWT algorithm confusion and how dangerous is it?

JWT algorithm confusion is an attack that exploits how some JWT libraries handle the algorithm field in a token's header. The most common form: an attacker changes the algorithm from RS256 (asymmetric, requires a private key to sign) to HS256 (symmetric, uses a shared secret). If the server's public key is publicly accessible — which it often is, by design — the attacker can sign a token with that public key and the server may accept it, because it's now expecting an HS256 signature rather than an RS256 one. The impact is total: the attacker can forge tokens for any user, including administrators. It's a critical vulnerability that results from libraries not enforcing the expected algorithm explicitly.

What does an API security test actually look for that automated scanners miss?

Automated scanners are good at finding missing headers, obvious injection patterns, and rate limiting gaps. They can't understand your application's business logic. BOLA is largely invisible to scanners because the scanner doesn't know which user IDs belong to which accounts. Business-logic flaws — being able to apply the same discount twice, transfer money you don't own, access another organisation's resources in a multi-tenant system, or bypass a multi-step workflow — require a tester who understands how the API is supposed to work and can identify how it can be abused.

What is the difference between API authentication and API authorisation?

Authentication answers 'who are you?' — verifying identity through an API key, OAuth token, JWT, or session cookie. Authorisation answers 'what are you allowed to do?' — checking whether the authenticated identity has permission to access a specific resource or perform a specific action. The vast majority of API security failures are authorisation failures, not authentication ones. An attacker with a valid account can often access other users' data or admin-only functionality simply because the authorisation checks are missing, inconsistent, or only applied at the route level rather than the object level.

Do GraphQL APIs have different security risks than REST APIs?

Same underlying risks, some unique attack surfaces. BOLA, authentication failures, and excessive data exposure all apply equally. GraphQL-specific risks include: introspection being enabled in production (revealing your entire API schema to anyone who queries it), deeply nested queries that can exhaust server resources without rate limiting, batch query attacks that amplify the impact of BOLA, and alias-based rate limit bypasses where multiple queries are bundled into a single request. GraphQL also tends to return significantly more data than REST equivalents by default, which increases the surface area for data exposure.