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

  1. Broken Object Level Authorization (BOLA/IDOR)
  2. Broken Authentication
  3. Broken Object Property Level Authorization
  4. Unrestricted Resource Consumption
  5. Broken Function Level Authorization
  6. Unrestricted Access to Sensitive Business Flows
  7. Server Side Request Forgery (SSRF)
  8. Security (as discussed in OWASP ZAP Automation: Security Scanning in CI/CD) Misconfiguration
  9. Improper Inventory Management
  10. 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
ScenarioRecommended Approach
Public API with sensitive dataFull security suite: OWASP Top 10, pen testing, automated scanning
Internal microservicesBOLA/IDOR testing, authentication validation, basic injection tests
Third-party API integrationFocus on credential security, rate limit handling
Mobile app backendJWT security, token storage, session management
B2B API productAPI key security, customer isolation, audit logging

Measuring Success

MetricBefore TestingTargetHow to Track
OWASP Top 10 CoverageUnknown100% testedSecurity test checklist
BOLA/IDOR VulnerabilitiesDiscovered in prod0 in prodPen test reports
Authentication Bypass AttemptsNot monitored100% blockedWAF/API gateway logs
Injection Attack DetectionVariable< 1ms detectionSecurity monitoring
Time to Remediate CriticalDays/Weeks< 24 hoursIncident 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