TL;DR
- Test BOLA/IDOR on every endpoint—it’s the #1 API vulnerability (OWASP API Security Top 10 2023)
- JWT testing must cover algorithm confusion, weak secrets, and token tampering—not just expiration
- Never accept API keys in URLs; test that rate limiting works per-key, not just per-IP
Best for: Public APIs, multi-tenant systems, APIs handling sensitive data (PII, financial, health)
Skip if: Internal-only APIs with trusted clients, early prototyping phase
Read time: 18 minutes
API security testing validates authentication, authorization, input validation, and data protection mechanisms. For QA engineers, understanding security vulnerabilities is essential to prevent unauthorized access and data breaches. If you’re building a solid foundation in API testing, explore our API Testing Mastery guide for comprehensive techniques. For teams working with microservices, API Testing Architecture covers distributed security patterns. Performance testing tools like those discussed in API Performance Testing can also help identify rate limiting effectiveness.
API Security Fundamentals
API security testing validates authentication, authorization, input validation, and data protection mechanisms. For QA engineers, thorough API security testing (as discussed in Mobile App Security Testing: iOS and Android Complete Guide) prevents unauthorized access, data breaches, and service abuse.
OWASP API Security Top 10
- Broken Object Level Authorization (BOLA/IDOR)
- Broken Authentication
- Broken Object Property Level Authorization
- Unrestricted Resource Consumption
- Broken Function Level Authorization
- Unrestricted Access to Sensitive Business Flows
- Server Side Request Forgery (SSRF)
- Security (as discussed in OWASP ZAP Automation: Security Scanning in CI/CD) Misconfiguration
- Improper Inventory Management
- Unsafe Consumption of APIs
OAuth 2.0 Testing
Authorization Code Flow
# Test OAuth authorization code flow
import requests
from urllib.parse import urlencode, parse_qs
class OAuthFlowTester:
def __init__(self, client_id, client_secret, auth_url, token_url):
self.client_id = client_id
self.client_secret = client_secret
self.auth_url = auth_url
self.token_url = token_url
def test_authorization_flow(self):
"""Test complete OAuth flow"""
# Step 1: Get authorization code
params = {
'response_type': 'code',
'client_id': self.client_id,
'redirect_uri': 'http://localhost:3000/callback',
'scope': 'read write',
'state': 'random_state_string'
}
auth_request = f"{self.auth_url}?{urlencode(params)}"
print(f"Authorization URL: {auth_request}")
# Simulate user authorization (manual step in real testing)
auth_code = input("Enter authorization code: ")
# Step 2: Exchange code for token
token_response = requests.post(self.token_url, data={
'grant_type': 'authorization_code',
'code': auth_code,
'redirect_uri': 'http://localhost:3000/callback',
'client_id': self.client_id,
'client_secret': self.client_secret
})
assert token_response.status_code == 200, "Token exchange failed"
tokens = token_response.json()
assert 'access_token' in tokens
assert 'refresh_token' in tokens
print("✓ OAuth flow successful")
return tokens
def test_token_refresh(self, refresh_token):
"""Test refresh token flow"""
response = requests.post(self.token_url, data={
'grant_type': 'refresh_token',
'refresh_token': refresh_token,
'client_id': self.client_id,
'client_secret': self.client_secret
})
assert response.status_code == 200
new_tokens = response.json()
assert 'access_token' in new_tokens
print("✓ Token refresh successful")
def test_invalid_client_credentials(self):
"""Test with invalid credentials"""
response = requests.post(self.token_url, data={
'grant_type': 'client_credentials',
'client_id': 'invalid_client',
'client_secret': 'invalid_secret'
})
assert response.status_code == 401, "Should reject invalid credentials"
print("✓ Invalid credentials properly rejected")
OAuth Security Tests
def test_oauth_security():
"""Test OAuth security vulnerabilities"""
# Test 1: State parameter (CSRF protection)
def test_state_parameter():
# Request without state
response = requests.get(auth_url, params={
'response_type': 'code',
'client_id': client_id,
'redirect_uri': redirect_uri
# Missing 'state' parameter
})
# Should enforce state parameter
assert 'error' in response.url or response.status_code == 400
# Test 2: Redirect URI validation
def test_redirect_uri_validation():
# Attempt redirect URI manipulation
malicious_redirect = 'http://evil.com/callback'
response = requests.get(auth_url, params={
'response_type': 'code',
'client_id': client_id,
'redirect_uri': malicious_redirect,
'state': 'test'
})
# Should reject unregistered redirect URIs
assert response.status_code != 302 or 'evil.com' not in response.headers.get('Location', '')
# Test 3: Token expiration
def test_token_expiration():
# Use expired token
expired_token = 'expired_token_here'
response = requests.get(
'https://api.example.com/protected',
headers={'Authorization': f'Bearer {expired_token}'}
)
assert response.status_code == 401
assert 'token expired' in response.json().get('error', '').lower()
test_state_parameter()
test_redirect_uri_validation()
test_token_expiration()
print("✓ OAuth security tests passed")
JWT Testing
JWT Structure and Validation
// JWT testing utilities
const jwt = require('jsonwebtoken');
const crypto = require('crypto');
class JWTTester {
constructor(secret) {
this.secret = secret;
}
// Test 1: Valid JWT
testValidJWT() {
const payload = {
userId: 123,
username: 'testuser',
role: 'user'
};
const token = jwt.sign(payload, this.secret, { expiresIn: '1h' });
try {
const decoded = jwt.verify(token, this.secret);
console.log('✓ Valid JWT verified successfully');
return decoded;
} catch (err) {
console.error('✗ JWT verification failed:', err.message);
}
}
// Test 2: Expired JWT
testExpiredJWT() {
const token = jwt.sign({ userId: 123 }, this.secret, { expiresIn: '-1s' });
try {
jwt.verify(token, this.secret);
console.error('✗ Expired JWT was accepted!');
} catch (err) {
if (err.name === 'TokenExpiredError') {
console.log('✓ Expired JWT properly rejected');
}
}
}
// Test 3: Algorithm confusion attack
testAlgorithmConfusion() {
// Generate token with 'none' algorithm
const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' })).toString('base64');
const payload = Buffer.from(JSON.stringify({ userId: 123, role: 'admin' })).toString('base64');
const maliciousToken = `${header}.${payload}.`;
try {
jwt.verify(maliciousToken, this.secret);
console.error('✗ Algorithm confusion attack succeeded!');
} catch (err) {
console.log('✓ Algorithm confusion attack prevented');
}
}
// Test 4: Weak secret
testWeakSecret() {
const weakSecrets = ['secret', '123456', 'password', 'jwt_secret'];
const token = jwt.sign({ userId: 123 }, 'weak_secret');
for (const secret of weakSecrets) {
try {
jwt.verify(token, secret);
console.error(`✗ JWT cracked with weak secret: ${secret}`);
return;
} catch (err) {
// Continue trying
}
}
console.log('✓ JWT not vulnerable to weak secret');
}
// Test 5: Token tampering
testTokenTampering() {
const token = jwt.sign({ userId: 123, role: 'user' }, this.secret);
// Attempt to modify payload
const parts = token.split('.');
const payload = JSON.parse(Buffer.from(parts[1], 'base64').toString());
payload.role = 'admin'; // Escalate privileges
const tamperedPayload = Buffer.from(JSON.stringify(payload)).toString('base64');
const tamperedToken = `${parts[0]}.${tamperedPayload}.${parts[2]}`;
try {
jwt.verify(tamperedToken, this.secret);
console.error('✗ Tampered JWT was accepted!');
} catch (err) {
console.log('✓ Tampered JWT properly rejected');
}
}
}
// Run tests
const tester = new JWTTester('your-secret-key');
tester.testValidJWT();
tester.testExpiredJWT();
tester.testAlgorithmConfusion();
tester.testWeakSecret();
tester.testTokenTampering();
API Key Security Testing
API Key Management
# API key security tests
import requests
import hashlib
class APIKeyTester:
def __init__(self, base_url):
self.base_url = base_url
def test_api_key_in_header(self, api_key):
"""Test API key in header (secure)"""
response = requests.get(
f"{self.base_url}/api/data",
headers={'X-API-Key': api_key}
)
assert response.status_code == 200
print("✓ API key in header works")
def test_api_key_in_url(self, api_key):
"""Test API key in URL (insecure - should be rejected)"""
response = requests.get(
f"{self.base_url}/api/data?api_key={api_key}"
)
# Should reject API keys in URL for security
(as discussed in [Penetration Testing Basics for QA Testers](/blog/penetration-testing-basics)) if response.status_code == 200:
print("✗ WARNING: API key accepted in URL (security risk)")
else:
print("✓ API key in URL properly rejected")
def test_api_key_rotation(self, old_key, new_key):
"""Test API key rotation"""
# Old key should be revoked
response = requests.get(
f"{self.base_url}/api/data",
headers={'X-API-Key': old_key}
)
assert response.status_code == 401
# New key should work
response = requests.get(
f"{self.base_url}/api/data",
headers={'X-API-Key': new_key}
)
assert response.status_code == 200
print("✓ API key rotation successful")
def test_rate_limiting(self, api_key):
"""Test rate limiting per API key"""
rate_limit = 100 # requests per minute
responses = []
for i in range(rate_limit + 10):
response = requests.get(
f"{self.base_url}/api/data",
headers={'X-API-Key': api_key}
)
responses.append(response.status_code)
# Should hit rate limit
assert 429 in responses, "Rate limiting not enforced"
print("✓ Rate limiting enforced")
Input Validation Testing
SQL Injection
# SQL injection tests
def test_sql_injection():
payloads = [
"' OR '1'='1",
"'; DROP TABLE users--",
"' UNION SELECT * FROM users--",
"1' AND '1'='1",
"admin'--"
]
for payload in payloads:
response = requests.post(
'https://api.example.com/login',
json={'username': payload, 'password': 'test'}
)
# Should not return successful login
assert response.status_code != 200 or 'token' not in response.json()
# Should not expose database errors
assert 'sql' not in response.text.lower()
assert 'syntax error' not in response.text.lower()
print("✓ SQL injection tests passed")
NoSQL Injection
# MongoDB injection tests
def test_nosql_injection():
payloads = [
{"$ne": None},
{"$gt": ""},
{"$regex": ".*"},
{"$where": "1==1"}
]
for payload in payloads:
response = requests.post(
'https://api.example.com/search',
json={'username': payload}
)
# Should not return unauthorized data
assert response.status_code in [400, 422] # Bad request
print("✓ NoSQL injection tests passed")
Authorization Testing (BOLA/IDOR)
# Test Broken Object Level Authorization
def test_bola():
"""Test IDOR vulnerability"""
# User A's token
token_a = 'user_a_token'
# User B's resource
user_b_resource_id = 456
# Attempt to access User B's resource with User A's token
response = requests.get(
f'https://api.example.com/users/{user_b_resource_id}/profile',
headers={'Authorization': f'Bearer {token_a}'}
)
# Should return 403 Forbidden
assert response.status_code == 403, "IDOR vulnerability detected!"
print("✓ BOLA/IDOR protection working")
# Test with User A's own resource
user_a_resource_id = 123
response = requests.get(
f'https://api.example.com/users/{user_a_resource_id}/profile',
headers={'Authorization': f'Bearer {token_a}'}
)
assert response.status_code == 200
print("✓ Authorized access working")
Automated API Security Scanning
# Using OWASP ZAP API scan
docker run -v $(pwd):/zap/wrk:rw owasp/zap2docker-stable \
zap-api-scan.py \
-t https://api.example.com \
-f openapi \
-d /zap/wrk/openapi.json \
-r /zap/wrk/api-security-report.html
# Using Postman Newman with security tests
newman run api-security-tests.json \
--environment production.json \
--reporters cli,html \
--reporter-html-export security-report.html
AI-Assisted Approaches
Security testing can be enhanced with AI tools for vulnerability detection and test generation.
What AI does well:
- Generate security test payloads from OWASP guidelines
- Analyze API specifications for potential authorization gaps
- Create comprehensive injection test cases (SQL, NoSQL, XSS)
- Identify missing security headers in API responses
- Generate JWT manipulation test scenarios
What still needs humans:
- Understanding business context to identify sensitive data flows
- Validating that security controls align with compliance requirements (GDPR, HIPAA, PCI-DSS)
- Assessing risk severity based on business impact
- Designing attack scenarios that combine multiple vulnerabilities
- Verifying security fixes don’t break legitimate functionality
Useful prompts:
Analyze this API specification and identify potential BOLA/IDOR vulnerabilities.
For each endpoint that accesses user-specific resources, generate test cases
that verify proper authorization checks.
Generate a comprehensive JWT security test suite including: algorithm confusion
attacks, expired token handling, signature tampering, and weak secret detection.
Include both Python and JavaScript implementations.
When to Invest in API Security Testing
Security testing is essential when:
- APIs handle sensitive data (PII, financial, health records)
- Public-facing APIs accessible to third-party developers
- Multi-tenant systems where data isolation is critical
- APIs processing payments or authentication
- Compliance requirements (SOC2, HIPAA, PCI-DSS, GDPR)
- After security incidents or vulnerability disclosures
Consider lighter approaches when:
- Internal-only APIs with network-level access controls
- Early prototyping where security isn’t configured yet
- Read-only APIs returning public data
- Development environments with no production data
| Scenario | Recommended Approach |
|---|---|
| Public API with sensitive data | Full security suite: OWASP Top 10, pen testing, automated scanning |
| Internal microservices | BOLA/IDOR testing, authentication validation, basic injection tests |
| Third-party API integration | Focus on credential security, rate limit handling |
| Mobile app backend | JWT security, token storage, session management |
| B2B API product | API key security, customer isolation, audit logging |
Measuring Success
| Metric | Before Testing | Target | How to Track |
|---|---|---|---|
| OWASP Top 10 Coverage | Unknown | 100% tested | Security test checklist |
| BOLA/IDOR Vulnerabilities | Discovered in prod | 0 in prod | Pen test reports |
| Authentication Bypass Attempts | Not monitored | 100% blocked | WAF/API gateway logs |
| Injection Attack Detection | Variable | < 1ms detection | Security monitoring |
| Time to Remediate Critical | Days/Weeks | < 24 hours | Incident tracking |
Warning signs your security testing isn’t working:
- Security vulnerabilities discovered by external researchers
- Authentication bypass found in production
- Data leaks or unauthorized access incidents
- Compliance audit failures
- Injection attacks succeeding against APIs
- API keys or tokens exposed in logs or URLs
Conclusion
API security testing is critical for protecting sensitive data and preventing unauthorized access. By systematically testing authentication mechanisms, authorization controls, input validation, and rate limiting, QA engineers ensure APIs resist common attacks.
Key Takeaways:
- Test all OAuth flows and token handling
- Validate JWT signatures and expiration
- Never expose API keys in URLs or logs
- Implement and test rate limiting
- Test for BOLA/IDOR vulnerabilities
- Validate all input against injection attacks
- Use automated tools like OWASP ZAP for comprehensive scans
Official Resources
See Also
- API Testing Mastery: From REST to Contract Testing - Complete guide covering REST, GraphQL, and gRPC testing fundamentals
- REST Assured: Java-Based API Testing Framework - Learn programmatic API testing with Java for security validation
- GraphQL Testing Guide - Security considerations specific to GraphQL APIs
- CI/CD Pipeline Optimization for QA Teams - Integrate security testing into your pipelines
- Mobile Testing 2025: iOS, Android and Beyond - Mobile app security testing strategies
