Back to Blog

API Security Testing: 8 Vulnerabilities Your QA Team Must Catch Before Hackers Do

Master API security testing with this comprehensive guide covering authentication, authorization, OWASP API Top 10, and practical testing strategies for REST and GraphQL APIs.

ScanlyApp Team

Published

13 min read

Reading time

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:

  1. Postman/Insomnia - Manual testing and test automation
  2. OWASP ZAP - Open-source security scanner
  3. Burp Suite - Comprehensive security testing platform
  4. ScanlyApp - Automated continuous API testing and monitoring
  5. JWT.io - JWT token inspection and debugging
  6. 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

  1. Test Early and Often: Integrate security testing from the design phase through production
  2. Automate Where Possible: Manual testing catches some issues, but automation ensures consistency
  3. Use Real-World Attack Patterns: Base tests on actual attack vectors from OWASP and CVE databases
  4. Test All Authentication Methods: OAuth, JWT, API keys, Basic Auth—each has unique vulnerabilities
  5. Don't Trust Client-Side Validation: Always test server-side validation independently
  6. Test for Business Logic Flaws: Not all vulnerabilities are technical; some are logical
  7. Monitor Production APIs: Security testing doesn't end at deployment
  8. 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.

Related Posts