API Security Testing: A Practical Guide for Developers
Learn API security testing with practical examples. Cover authentication flaws, injection attacks, and data exposure prevention.
APIs are the connective tissue of modern software. They power mobile apps, connect microservices, enable third-party integrations, and expose business logic to the outside world. That ubiquity makes them the single most attractive attack vector for malicious actors.
According to the OWASP API Security Top 10, the majority of API breaches stem from predictable, preventable vulnerabilities: broken authentication, excessive data exposure, lack of rate limiting, and injection attacks. These are not exotic zero-day exploits. They are configuration mistakes and missing validation checks that any developer can test for -- if they know what to look for.
This guide covers the practical security tests every developer should run against their APIs, with concrete examples you can execute today.
Why API Security Testing Matters
Traditional web application security testing focuses on browser-based vulnerabilities: XSS in rendered HTML, CSRF tokens in forms, cookie security. API security testing is different. APIs communicate in structured data (JSON, XML, Protocol Buffers), are often stateless, and expose functionality directly without a browser intermediary.
This creates a distinct threat model:
- No browser protections. APIs do not benefit from browser-enforced security policies like Content Security Policy or SameSite cookies by default.
- Direct access to business logic. An API endpoint that transfers funds or deletes accounts is one HTTP request away from any client that has the URL.
- Machine-speed attacks. Without rate limiting, an attacker can send thousands of requests per second to brute-force credentials or enumerate resources.
- Data exposure at scale. A single misconfigured list endpoint can leak millions of records.
The OWASP API Security Top 10 identifies the most critical risks: Broken Object Level Authorization (BOLA), Broken Authentication, Broken Object Property Level Authorization, Unrestricted Resource Consumption, Broken Function Level Authorization, Server-Side Request Forgery, Security Misconfiguration, Lack of Protection from Automated Threats, Improper Asset Management, and Unsafe Consumption of APIs.
You do not need to test for all of these at once. Start with the fundamentals below and expand your coverage over time.
Authentication and Authorization Testing
Broken authentication is consistently ranked as one of the most dangerous API vulnerabilities. If an attacker can bypass your auth, every other security control is irrelevant.
Tests to Run
Missing authentication headers:
# Send a request to a protected endpoint without any auth
curl -X GET https://api.example.com/v1/users/me \
-H "Content-Type: application/json"
# Expected: 401 Unauthorized
Invalid and expired tokens:
# Expired JWT token
curl -X GET https://api.example.com/v1/users/me \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwiZXhwIjoxNjAwMDAwMDAwfQ.invalid"
# Malformed token
curl -X GET https://api.example.com/v1/users/me \
-H "Authorization: Bearer not-a-real-token"
# Both should return 401 Unauthorized
Broken Object Level Authorization (BOLA):
# Authenticate as User A, then try to access User B's data
curl -X GET https://api.example.com/v1/users/USER_B_ID/profile \
-H "Authorization: Bearer USER_A_TOKEN"
# Expected: 403 Forbidden (not 200 OK with User B's data)
JWT validation weaknesses:
- Change the
algfield in the JWT header tononeand send an unsigned token - Modify the payload claims (e.g., change
role: "user"torole: "admin") without re-signing - Use an expired token and verify the server rejects it
OAuth flow validation:
- Submit an authorization code more than once (replay attack)
- Use a redirect URI that differs from the registered one
- Attempt token exchange with a mismatched PKCE code verifier
For teams managing authentication tokens across environments, storing credentials securely is essential. See our guide on environment variables best practices for strategies on managing auth tokens without exposing them.
What to Look For
Any response that returns 200 OK when it should return 401 or 403 is a critical finding.
Also watch for error messages that leak information -- a response like
"error": "User not found" versus "error": "Invalid password" tells an attacker whether a
username exists.
Input Validation and Injection Testing
APIs that trust client input without validation are vulnerable to injection attacks. Even if your front-end validates input, the API must enforce its own validation -- attackers bypass front-ends entirely.
SQL Injection
# Attempt SQL injection in a query parameter
curl -X GET "https://api.example.com/v1/users?search=admin'%20OR%201=1--" \
-H "Authorization: Bearer $TOKEN"
# Attempt SQL injection in a JSON body
curl -X POST https://api.example.com/v1/users/search \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"username": "admin\" OR \"1\"=\"1"}'
# Expected: 400 Bad Request (not a data dump)
XSS via API Responses
# Submit a script tag in a user-controlled field
curl -X POST https://api.example.com/v1/comments \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"body": "<script>alert(document.cookie)</script>"}'
# The API should sanitize or escape the input
Even if the API does not render HTML, stored XSS payloads in API responses can be executed when a front-end application displays the data. The API should sanitize input at the boundary.
Parameter Tampering
# Try to set admin privileges via a hidden field
curl -X POST https://api.example.com/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name": "New User", "email": "[email protected]", "role": "admin"}'
# Expected: role field should be ignored or return 403
# Manipulate IDs in request bodies
curl -X PUT https://api.example.com/v1/orders/123 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"id": "456", "status": "shipped"}'
# Expected: server should use the URL parameter ID, not the body ID
Request Body Manipulation
- Send a
Content-Type: application/jsonheader with XML in the body - Send extremely large payloads (1 MB+ JSON bodies) to test for denial-of-service
- Include unexpected nested objects or arrays where the API expects flat values
- Submit null bytes, unicode control characters, and other encoding edge cases
Rate Limiting and Throttle Testing
Without rate limiting, your API is vulnerable to brute-force attacks, credential stuffing, and resource exhaustion.
Tests to Run
# Test login endpoint rate limiting
for i in $(seq 1 20); do
curl -s -o /dev/null -w "%{http_code}\n" \
-X POST https://api.example.com/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"email": "[email protected]", "password": "wrong-password-'$i'"}'
done
# Expected: After N attempts, responses should change to 429 Too Many Requests
What to verify:
- The API returns
429 Too Many Requestsafter exceeding the limit - The
Retry-Afterheader is present and contains a reasonable value - Rate limits apply per-user or per-IP, not globally
- Different endpoints have appropriate limits (login should be stricter than read endpoints)
- Rate limits cannot be bypassed by rotating headers like
X-Forwarded-For
Check rate limit headers in responses:
HTTP/1.1 200 OK
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 95
X-RateLimit-Reset: 1706140800
If these headers are missing, the API likely does not implement rate limiting -- that is a finding worth reporting.
Data Exposure Testing
APIs frequently return more data than the client needs. This excessive data exposure is a top OWASP API risk because developers often serialize entire database objects instead of selecting specific fields.
Tests to Run
Check for excessive fields in responses:
# Request a user profile and inspect the response
curl -X GET https://api.example.com/v1/users/me \
-H "Authorization: Bearer $TOKEN" | jq .
Look for fields that should not be exposed to the client:
- Internal database IDs (
_id,internal_id) - Password hashes (
password_hash,password_digest) - Infrastructure details (
created_by_service,database_shard) - Other users' data in nested objects
- Soft-delete flags (
is_deleted,deleted_at) - Debug or internal metadata
Check list endpoints for data leakage:
# Request a list and check if it returns other users' records
curl -X GET "https://api.example.com/v1/orders" \
-H "Authorization: Bearer $USER_A_TOKEN" | jq '.[].user_id' | sort -u
# Expected: only User A's ID should appear
Inspect error responses for information disclosure:
# Trigger an error and check the response
curl -X GET "https://api.example.com/v1/users/nonexistent-id" \
-H "Authorization: Bearer $TOKEN"
Verbose error messages are a goldmine for attackers. Watch for:
- Stack traces in production responses
- Database error messages (
SQLSTATE[42S02]: Table 'users' not found) - Internal file paths (
/var/www/app/src/controllers/UserController.js:42) - Framework and version information (
Express 4.18.2,Django 4.2)
The API should return generic error messages in production: "error": "Resource not found" -- not
the underlying implementation details.
For a deeper look at keeping sensitive data out of your API workflow entirely, read Privacy-First API Development.
CORS and Header Security Testing
Cross-Origin Resource Sharing misconfigurations are among the most common API security issues, and they are easy to test for.
CORS Tests
# Test with a malicious origin
curl -X OPTIONS https://api.example.com/v1/users \
-H "Origin: https://evil-site.com" \
-H "Access-Control-Request-Method: GET" \
-v 2>&1 | grep -i "access-control"
# Expected: Origin should NOT be reflected back if it's not whitelisted
# Dangerous: Access-Control-Allow-Origin: https://evil-site.com
# Dangerous: Access-Control-Allow-Origin: *
What to verify:
Access-Control-Allow-Origindoes not use a wildcard (*) for authenticated endpoints- The server does not blindly reflect the
Originheader value Access-Control-Allow-Credentials: trueis never combined withAccess-Control-Allow-Origin: *- Preflight responses do not allow dangerous methods (PUT, DELETE) from untrusted origins
Security Headers
curl -I https://api.example.com/v1/users \
-H "Authorization: Bearer $TOKEN"
Check for the presence of these security headers:
| Header | Expected Value | Purpose |
|---|---|---|
X-Content-Type-Options | nosniff | Prevents MIME-type sniffing |
X-Frame-Options | DENY or SAMEORIGIN | Prevents clickjacking |
Strict-Transport-Security | max-age=31536000; includeSubDomains | Enforces HTTPS |
Cache-Control | no-store (for sensitive data) | Prevents caching of sensitive responses |
Content-Type | Correct type with charset | Prevents content-type confusion |
Content-Type Validation
# Send a request with a mismatched content type
curl -X POST https://api.example.com/v1/users \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: text/plain" \
-d '{"name": "Test User"}'
# Expected: 415 Unsupported Media Type (not silent acceptance)
The API should reject requests with unexpected content types rather than attempting to parse them.
Transport Security
All API communication should happen over HTTPS. This is non-negotiable.
Tests to Run
# Attempt HTTP connection (should redirect to HTTPS or refuse)
curl -v http://api.example.com/v1/health 2>&1 | grep -E "< HTTP|Location:"
# Expected: 301 redirect to HTTPS or connection refused
# Check TLS version (reject TLS 1.0 and 1.1)
curl -v --tlsv1.0 https://api.example.com/v1/health 2>&1
# Expected: Connection failure (TLS 1.0 should not be accepted)
# Verify HSTS header
curl -I https://api.example.com/v1/health 2>&1 | grep -i strict-transport
# Expected: Strict-Transport-Security: max-age=31536000; includeSubDomains
What to verify:
- HTTP requests are either rejected or permanently redirected (301) to HTTPS
- TLS 1.0 and 1.1 are disabled; only TLS 1.2+ is accepted
- HSTS header is present with a long
max-agevalue - SSL certificates are valid and not self-signed in production
- Certificate pinning is implemented for mobile API clients
Building Security Testing Into Your Workflow
Security testing should not be a one-time audit. It should be woven into your daily development workflow so that vulnerabilities are caught as they are introduced, not months later in a penetration test.
Test Security in Your API Client
Your API client is the most natural place to run security tests. Instead of only testing happy paths, build a dedicated "Security Tests" folder in your collections:
My API Collection/
Authentication/
Valid login
Invalid credentials
Expired token
Missing auth header
BOLA - access other user's data
Input Validation/
SQL injection attempts
XSS payload in fields
Oversized request body
Unexpected content types
Authorization/
User accessing admin endpoints
Accessing other user's resources
Privilege escalation attempts
Run this security collection after every significant API change. In RESTK, you can execute an entire collection with a single command, making it practical to include security tests in your regular testing cadence. See the full list of RESTK features that support this workflow.
Environment-Based Auth Testing
Use separate environments to test different authorization levels:
// admin-env.json
{
"baseUrl": "https://staging-api.example.com",
"authToken": "Bearer ADMIN_TOKEN",
"testUserId": "admin-user-id"
}
// regular-user-env.json
{
"baseUrl": "https://staging-api.example.com",
"authToken": "Bearer USER_TOKEN",
"testUserId": "regular-user-id"
}
// unauthenticated-env.json
{
"baseUrl": "https://staging-api.example.com",
"authToken": "",
"testUserId": ""
}
Run the same collection against each environment. Requests that succeed with admin credentials
should fail with regular user credentials and return 401 with no credentials. This approach
catches authorization gaps systematically rather than through ad-hoc testing.
Automate Security Checks in CI
Export your security test collection and run it as a CI/CD pipeline step:
- name: Run API security tests
run: |
newman run security-tests.json \
--environment staging.json \
--reporters cli,junit \
--reporter-junit-export security-results.xml
Block deployments when security tests fail. A broken security test is more critical than a broken feature test.
Keeping Credentials Safe While Testing
There is an irony in API security testing: the process of testing security often involves handling the very secrets you are trying to protect. Authentication tokens, API keys, OAuth client secrets, and admin credentials all flow through your testing tool.
This is where the security posture of your API client itself matters. If your tool syncs collections and environments to the cloud by default, your test credentials -- including production tokens and admin keys -- are stored on the vendor's servers. If that vendor is breached, your credentials are exposed.
RESTK takes a fundamentally different approach with a privacy-first architecture:
- Local storage by default. All requests, responses, collections, environments, and credentials are stored on your local filesystem. Nothing leaves your machine unless you explicitly enable cloud sync.
- AES-256 encryption. Sensitive values are encrypted at rest using AES-256 encryption. Even if someone gains access to your local storage files, secrets remain encrypted.
- Native keychain integration. API keys and tokens can be stored in macOS Keychain, protected by the same encryption that guards your system passwords.
- No content telemetry. RESTK never collects the content of your requests, responses, headers, or environment variables. Your security test payloads -- including any injection strings, auth tokens, and sensitive test data -- stay on your device.
When you are running injection tests with payloads like '; DROP TABLE users;-- or testing with
production admin tokens, the last thing you want is that data sitting on a vendor's cloud server.
Local-first storage eliminates that risk entirely.
A Security Testing Checklist
Use this as a starting point for your API security testing practice:
- All endpoints return
401when no authentication is provided - Expired and malformed tokens are rejected with
401 - Users cannot access other users' resources (BOLA testing)
- Role-based restrictions are enforced (regular users cannot hit admin endpoints)
- SQL injection payloads return
400, not data - XSS payloads are sanitized or escaped in stored responses
- Unexpected fields in request bodies are ignored (no mass assignment)
- Rate limiting is active on authentication endpoints
- Rate limit headers are present in responses
- Responses do not contain internal fields (password hashes, internal IDs)
- Error messages do not expose stack traces or infrastructure details
- CORS is configured to reject unauthorized origins
- Security headers are present (HSTS, X-Content-Type-Options, etc.)
- HTTP requests are redirected to HTTPS
- TLS 1.0 and 1.1 are disabled
Start with authentication and authorization tests -- they have the highest impact. Add input validation and data exposure tests next. Layer in CORS, headers, and transport security as your coverage matures.
Security testing is not a one-time checkbox. It is a practice that improves with every iteration. Build it into your workflow, automate what you can, and keep your testing tools as secure as the APIs you are protecting.
Related reading: