API Security Testing: 8 Vulnerabilities Your QA Team Must Catch Before Hackers Do
APIs are the backbone of modern applications, but they're also one of the most vulnerable attack surfaces. According to Gartner, API attacks are the most-frequent attack vector, causing data breaches for enterprise web applications. As a QA professional, understanding API security testing is no longer optional—it's essential.
This comprehensive guide will walk you through everything you need to know about API security testing, from fundamental concepts to advanced techniques, with practical examples you can implement immediately.
Why API Security Testing Matters
APIs expose business logic and data directly to consumers. Unlike traditional web applications where the UI provides a natural barrier, APIs are designed for programmatic access, making them attractive targets for attackers. A single misconfigured endpoint can expose sensitive data, allow unauthorized actions, or bring down your entire system.
Consider these real-world scenarios:
- An e-commerce API that doesn't validate user IDs, allowing customers to view other users' orders
- A REST API returning excessive data in responses, leaking internal system information
- A GraphQL endpoint vulnerable to query depth attacks, causing database overload
- JWT tokens with weak signing algorithms, allowing token forgery
The OWASP API Security Top 10
The OWASP API Security Top 10 provides a framework for understanding the most critical API security risks. Let's examine each one with testing strategies:
graph TD
A[OWASP API Top 10] --> B[API1: Broken Object Level Authorization]
A --> C[API2: Broken Authentication]
A --> D[API3: Broken Object Property Level Authorization]
A --> E[API4: Unrestricted Resource Access]
A --> F[API5: Broken Function Level Authorization]
A --> G[API6: Unrestricted Access to Sensitive Business Flows]
A --> H[API7: Server Side Request Forgery]
A --> I[API8: Security Misconfiguration]
A --> J[API9: Improper Inventory Management]
A --> K[API10: Unsafe Consumption of APIs]
API1: Broken Object Level Authorization (BOLA)
BOLA occurs when an API doesn't properly validate that a user should have access to a specific object. This is the most common and impactful API vulnerability.
Testing Strategy:
// Test: Accessing another user's resource
const user1Token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const user2ResourceId = '12345';
// User 1 attempts to access User 2's resource
const response = await fetch(`https://api.example.com/users/${user2ResourceId}/profile`, {
headers: {
Authorization: `Bearer ${user1Token}`,
},
});
// Expected: 403 Forbidden
// Vulnerable: 200 OK with User 2's data
console.log(`Status: ${response.status}`);
Test Cases:
- Access resources with sequential IDs (1, 2, 3...)
- Use UUIDs or GUIDs if supposed to be unpredictable
- Try accessing resources after revoking permissions
- Test with expired tokens
- Attempt cross-tenant data access in multi-tenant systems
API2: Broken Authentication
Authentication vulnerabilities allow attackers to compromise authentication tokens or exploit implementation flaws.
Testing JWT Security:
import jwt
import base64
# Test 1: Check for 'none' algorithm acceptance
header = base64.urlsafe_b64encode(b'{"alg":"none","typ":"JWT"}').decode('utf-8').rstrip('=')
payload = base64.urlsafe_b64encode(b'{"sub":"admin","role":"admin"}').decode('utf-8').rstrip('=')
malicious_token = f"{header}.{payload}."
# Test 2: Verify token expiration
token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
try:
decoded = jwt.decode(token, options={"verify_signature": False})
exp_time = decoded.get('exp')
current_time = time.time()
if exp_time and exp_time < current_time:
print("Token properly expired")
else:
print("WARNING: Expired token still accepted")
except jwt.ExpiredSignatureError:
print("Token expiration enforced correctly")
# Test 3: Weak signing key detection
common_secrets = ['secret', 'password', '123456', 'secret123']
for secret in common_secrets:
try:
decoded = jwt.decode(token, secret, algorithms=["HS256"])
print(f"CRITICAL: Weak secret detected: {secret}")
break
except jwt.InvalidSignatureError:
continue
API3: Broken Object Property Level Authorization
This vulnerability occurs when APIs expose more properties than necessary or allow modification of properties that should be restricted.
Test Case Example:
// Request: Update user profile
PUT /api/users/123
{
"name": "John Doe",
"email": "john@example.com",
"isAdmin": true, // Should not be user-modifiable
"accountBalance": 9999 // Should not be user-modifiable
}
// Test: Does the API ignore or process these sensitive fields?
Authentication Testing Strategies
Authentication is the foundation of API security. Here's a comprehensive testing matrix:
| Test Scenario | Expected Behavior | Test Method |
|---|---|---|
| No token provided | 401 Unauthorized | Remove Authorization header |
| Invalid token format | 401 Unauthorized | Send malformed token |
| Expired token | 401 Unauthorized | Use token with exp claim in past |
| Revoked token | 401 Unauthorized | Revoke token then attempt access |
| Wrong signature | 401 Unauthorized | Modify token signature |
| Missing required claims | 401 Unauthorized | Create token without sub/user_id |
| Token from different environment | 401 Unauthorized | Use production token on staging |
| Excessive token lifetime | Should expire in reasonable time | Check exp claim duration |
OAuth 2.0 Flow Testing
// Test OAuth 2.0 Authorization Code Flow
async function testOAuthFlow() {
// Step 1: Authorization request
const authUrl = 'https://auth.example.com/oauth/authorize';
const params = new URLSearchParams({
client_id: 'your_client_id',
redirect_uri: 'https://yourapp.com/callback',
response_type: 'code',
scope: 'read write',
state: 'random_state_value_' + Math.random(), // CSRF protection
});
// Step 2: Test redirect_uri validation
const maliciousParams = { ...params, redirect_uri: 'https://attacker.com' };
// Expected: Should reject unauthorized redirect_uri
// Step 3: Exchange code for token
const tokenResponse = await fetch('https://auth.example.com/oauth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
code: 'received_authorization_code',
redirect_uri: params.redirect_uri,
client_id: 'your_client_id',
client_secret: 'your_client_secret',
}),
});
// Test: Code reuse
const reuseAttempt = await fetch(/* same request as above */);
// Expected: Should fail - codes are single-use
}
Rate Limiting and DoS Protection Testing
APIs without rate limiting are vulnerable to denial-of-service attacks and resource exhaustion.
import asyncio
import aiohttp
import time
async def test_rate_limiting(url, token, requests_per_second=100):
"""
Test API rate limiting by sending rapid requests
"""
headers = {'Authorization': f'Bearer {token}'}
results = {
'total_requests': 0,
'successful': 0,
'rate_limited': 0,
'errors': 0
}
async with aiohttp.ClientSession() as session:
tasks = []
for i in range(requests_per_second):
task = asyncio.ensure_future(
make_request(session, url, headers, results)
)
tasks.append(task)
await asyncio.gather(*tasks)
print(f"Rate Limiting Test Results:")
print(f"Total Requests: {results['total_requests']}")
print(f"Successful (200): {results['successful']}")
print(f"Rate Limited (429): {results['rate_limited']}")
print(f"Errors: {results['errors']}")
# Verify rate limiting is in place
if results['rate_limited'] == 0:
print("⚠️ WARNING: No rate limiting detected!")
else:
print("✓ Rate limiting is active")
async def make_request(session, url, headers, results):
try:
async with session.get(url, headers=headers) as response:
results['total_requests'] += 1
if response.status == 200:
results['successful'] += 1
elif response.status == 429:
results['rate_limited'] += 1
# Check for Retry-After header
retry_after = response.headers.get('Retry-After')
if retry_after:
print(f"Rate limit hit. Retry after: {retry_after}s")
else:
results['errors'] += 1
except Exception as e:
results['errors'] += 1
print(f"Error: {e}")
Input Validation Testing
Insufficient input validation is a common vulnerability. Test all input fields for:
// SQL Injection Test Cases
const sqlInjectionPayloads = [
"' OR '1'='1",
"'; DROP TABLE users; --",
"1' UNION SELECT null, username, password FROM users--",
"admin'--",
"' OR 1=1--",
];
// NoSQL Injection Test Cases (MongoDB)
const noSqlInjectionPayloads = [{ $gt: '' }, { $ne: null }, { $regex: '.*' }];
// XSS Test Cases
const xssPayloads = [
"<script>alert('XSS')</script>",
"<img src=x onerror=alert('XSS')>",
"javascript:alert('XSS')",
"<svg/onload=alert('XSS')>",
];
// Test function
async function testInputValidation(endpoint, field, payloads) {
const results = [];
for (const payload of payloads) {
const testData = { [field]: payload };
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(testData),
});
results.push({
payload,
status: response.status,
vulnerable: response.status === 200, // Simplified check
});
}
return results;
}
GraphQL Security Testing
GraphQL APIs have unique security considerations due to their flexible query structure.
Query Depth Attack Testing
# Malicious deeply nested query
query DeeplyNested {
user(id: "1") {
posts {
comments {
author {
posts {
comments {
author {
posts {
# ... continues 50+ levels deep
}
}
}
}
}
}
}
}
}
Protection Test:
const { createComplexityLimitRule } = require('graphql-validation-complexity');
// Test that query complexity is limited
const complexityLimit = createComplexityLimitRule(1000, {
onCost: (cost) => {
console.log(`Query cost: ${cost}`);
},
});
// Expected: Queries exceeding limit should be rejected
GraphQL Introspection Testing
# Test if introspection is enabled in production
query IntrospectionQuery {
__schema {
types {
name
fields {
name
type {
name
}
}
}
}
}
Best Practice: Introspection should be disabled in production environments.
Security Testing Automation Framework
Here's a complete framework for automated API security testing:
import requests
from typing import List, Dict
from dataclasses import dataclass
@dataclass
class SecurityTestResult:
test_name: str
endpoint: str
passed: bool
severity: str
details: str
class APISecurityTester:
def __init__(self, base_url: str, auth_token: str):
self.base_url = base_url
self.auth_token = auth_token
self.results: List[SecurityTestResult] = []
def test_authentication(self):
"""Test authentication mechanisms"""
# Test 1: Access without token
response = requests.get(f"{self.base_url}/api/protected")
self.results.append(SecurityTestResult(
test_name="No Authentication Token",
endpoint="/api/protected",
passed=response.status_code == 401,
severity="HIGH",
details=f"Status: {response.status_code}"
))
# Test 2: Invalid token
headers = {"Authorization": "Bearer invalid_token_12345"}
response = requests.get(
f"{self.base_url}/api/protected",
headers=headers
)
self.results.append(SecurityTestResult(
test_name="Invalid Authentication Token",
endpoint="/api/protected",
passed=response.status_code == 401,
severity="HIGH",
details=f"Status: {response.status_code}"
))
def test_authorization(self, user_id: str, other_user_id: str):
"""Test authorization and BOLA vulnerabilities"""
headers = {"Authorization": f"Bearer {self.auth_token}"}
# Test: Access another user's resource
response = requests.get(
f"{self.base_url}/api/users/{other_user_id}/profile",
headers=headers
)
self.results.append(SecurityTestResult(
test_name="BOLA - Access Other User Resource",
endpoint=f"/api/users/{other_user_id}/profile",
passed=response.status_code in [403, 404],
severity="CRITICAL",
details=f"Status: {response.status_code}"
))
def test_rate_limiting(self, endpoint: str):
"""Test rate limiting implementation"""
headers = {"Authorization": f"Bearer {self.auth_token}"}
rapid_requests = 100
rate_limited_count = 0
for _ in range(rapid_requests):
response = requests.get(
f"{self.base_url}{endpoint}",
headers=headers
)
if response.status_code == 429:
rate_limited_count += 1
self.results.append(SecurityTestResult(
test_name="Rate Limiting",
endpoint=endpoint,
passed=rate_limited_count > 0,
severity="MEDIUM",
details=f"Rate limited {rate_limited_count}/{rapid_requests} requests"
))
def test_input_validation(self, endpoint: str):
"""Test input validation"""
headers = {
"Authorization": f"Bearer {self.auth_token}",
"Content-Type": "application/json"
}
malicious_payloads = [
{"username": "'; DROP TABLE users; --"},
{"email": "<script>alert('XSS')</script>"},
{"amount": -9999999}
]
for payload in malicious_payloads:
response = requests.post(
f"{self.base_url}{endpoint}",
json=payload,
headers=headers
)
# API should reject with 400 Bad Request
self.results.append(SecurityTestResult(
test_name=f"Input Validation - {list(payload.keys())[0]}",
endpoint=endpoint,
passed=response.status_code == 400,
severity="HIGH",
details=f"Payload: {payload}, Status: {response.status_code}"
))
def generate_report(self) -> Dict:
"""Generate security test report"""
total_tests = len(self.results)
passed_tests = sum(1 for r in self.results if r.passed)
failed_tests = total_tests - passed_tests
critical_failures = [r for r in self.results
if not r.passed and r.severity == "CRITICAL"]
return {
"summary": {
"total_tests": total_tests,
"passed": passed_tests,
"failed": failed_tests,
"pass_rate": f"{(passed_tests/total_tests)*100:.1f}%"
},
"critical_failures": critical_failures,
"all_results": self.results
}
# Usage
tester = APISecurityTester("https://api.example.com", "your_token_here")
tester.test_authentication()
tester.test_authorization("user123", "user456")
tester.test_rate_limiting("/api/search")
tester.test_input_validation("/api/users")
report = tester.generate_report()
print(f"Security Test Results: {report['summary']['pass_rate']} passed")
Security Testing Checklist
Use this comprehensive checklist for your API security testing:
| Category | Test Item | Priority |
|---|---|---|
| Authentication | No token returns 401 | Critical |
| Invalid token returns 401 | Critical | |
| Expired token returns 401 | Critical | |
| Token signature validation | Critical | |
| Weak secret detection | High | |
| Authorization | BOLA testing (access other user resources) | Critical |
| Privilege escalation attempts | Critical | |
| Role-based access control | High | |
| Cross-tenant data access | Critical | |
| Input Validation | SQL injection prevention | Critical |
| NoSQL injection prevention | Critical | |
| XSS prevention | High | |
| Command injection prevention | Critical | |
| File upload validation | High | |
| Rate Limiting | Request frequency limits | Medium |
| Retry-After header presence | Low | |
| Per-endpoint rate limiting | Medium | |
| Data Exposure | Sensitive data in responses | High |
| Detailed error messages | Medium | |
| API versioning in URLs | Low | |
| Transport Security | HTTPS enforcement | Critical |
| TLS version (1.2+) | High | |
| Certificate validation | High | |
| CORS | Origin validation | High |
| Credential handling | High |
Tools for API Security Testing
graph LR
A[API Security Testing Tools] --> B[Manual Testing]
A --> C[Automated Scanning]
A --> D[Continuous Monitoring]
B --> E[Postman]
B --> F[Insomnia]
B --> G[cURL]
C --> H[OWASP ZAP]
C --> I[Burp Suite]
C --> J[Nuclei]
D --> K[ScanlyApp]
D --> L[API Gateway Logs]
D --> M[SIEM Integration]
Tool Recommendations:
- Postman/Insomnia - Manual testing and test automation
- OWASP ZAP - Open-source security scanner
- Burp Suite - Comprehensive security testing platform
- ScanlyApp - Automated continuous API testing and monitoring
- JWT.io - JWT token inspection and debugging
- Nuclei - Fast, template-based vulnerability scanner
Integrating Security Testing into CI/CD
# .github/workflows/api-security-tests.yml
name: API Security Tests
on:
pull_request:
branches: [main, develop]
schedule:
- cron: '0 2 * * *' # Daily at 2 AM
jobs:
security-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
pip install requests pytest pytest-html
- name: Run authentication tests
run: |
pytest tests/security/test_authentication.py \
--html=report.html \
--self-contained-html
env:
API_BASE_URL: ${{ secrets.API_BASE_URL }}
TEST_TOKEN: ${{ secrets.TEST_TOKEN }}
- name: Run OWASP ZAP baseline scan
run: |
docker run -v $(pwd):/zap/wrk/:rw \
-t owasp/zap2docker-stable \
zap-baseline.py \
-t ${{ secrets.API_BASE_URL }} \
-r zap-report.html
- name: Upload security reports
if: always()
uses: actions/upload-artifact@v3
with:
name: security-reports
path: |
report.html
zap-report.html
- name: Fail on critical vulnerabilities
run: |
python scripts/check_critical_vulns.py zap-report.html
Best Practices for API Security Testing
- Test Early and Often: Integrate security testing from the design phase through production
- Automate Where Possible: Manual testing catches some issues, but automation ensures consistency
- Use Real-World Attack Patterns: Base tests on actual attack vectors from OWASP and CVE databases
- Test All Authentication Methods: OAuth, JWT, API keys, Basic Auth—each has unique vulnerabilities
- Don't Trust Client-Side Validation: Always test server-side validation independently
- Test for Business Logic Flaws: Not all vulnerabilities are technical; some are logical
- Monitor Production APIs: Security testing doesn't end at deployment
- Document and Share Findings: Create a knowledge base of vulnerabilities found and fixes applied
Conclusion
API security testing is a critical skill for modern QA professionals. By understanding common vulnerabilities, implementing comprehensive test strategies, and automating security checks in your CI/CD pipeline, you can significantly reduce the risk of security breaches.
Remember that security is not a one-time activity—it's an ongoing process. Regular security testing, combined with continuous monitoring and rapid response to new threats, creates a robust security posture for your APIs.
Start with the OWASP API Security Top 10, build automated test suites, and gradually expand your security testing coverage. Tools like ScanlyApp can help you maintain continuous security monitoring without the overhead of manual testing.
Sign up for ScanlyApp to automate your API security testing and catch vulnerabilities before they reach production.
Related articles: Also see mapping your API tests to the OWASP Top 10 vulnerability list, extending your security program from the API layer to the full application, and preventing broken access control as a leading OWASP risk.
