web application security

Testing API Security with OWASP Top 10

Systematically assessing REST and GraphQL API endpoints against the OWASP API Security Top 10 risks using automated and manual testing techniques.

api-securityburpsuitegraphqlowasppenetration-testingpostmanrest-api
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

When to Use

  • During authorized API penetration testing engagements
  • When assessing REST, GraphQL, or gRPC APIs for security vulnerabilities
  • Before deploying new API endpoints to production environments
  • When reviewing API security posture against the OWASP API Security Top 10 (2023)
  • For validating API gateway security controls and rate limiting effectiveness

Prerequisites

  • Authorization: Written scope document covering all API endpoints to be tested
  • Burp Suite Professional: For intercepting and modifying API requests
  • Postman: For organizing and executing API test collections
  • ffuf: For API endpoint and parameter fuzzing
  • curl/httpie: Command-line HTTP clients for manual testing
  • API documentation: Swagger/OpenAPI spec, GraphQL schema, or API docs
  • jq: JSON processor for parsing API responses (apt install jq)

Workflow

Step 1: Discover and Map API Endpoints

Enumerate all available API endpoints and understand the API surface.

# If OpenAPI/Swagger spec is available, download it
curl -s "https://api.target.example.com/swagger.json" | jq '.paths | keys[]'
curl -s "https://api.target.example.com/v2/api-docs" | jq '.paths | keys[]'
curl -s "https://api.target.example.com/openapi.yaml"
 
# Fuzz for API endpoints
ffuf -u "https://api.target.example.com/api/v1/FUZZ" \
  -w /usr/share/seclists/Discovery/Web-Content/api/api-endpoints.txt \
  -mc 200,201,204,301,401,403,405 \
  -fc 404 \
  -H "Content-Type: application/json" \
  -o api-enum.json -of json
 
# Fuzz for API versions
for v in v1 v2 v3 v4 beta internal admin; do
  status=$(curl -s -o /dev/null -w "%{http_code}" \
    "https://api.target.example.com/api/$v/users")
  echo "$v: $status"
done
 
# Check for GraphQL endpoint
for path in graphql graphiql playground query gql; do
  status=$(curl -s -o /dev/null -w "%{http_code}" \
    -X POST -H "Content-Type: application/json" \
    -d '{"query":"{__typename}"}' \
    "https://api.target.example.com/$path")
  echo "$path: $status"
done

Step 2: Test API1 - Broken Object Level Authorization (BOLA)

Test whether users can access objects belonging to other users by manipulating IDs.

# Authenticate as User A and get their resources
TOKEN_A="Bearer eyJhbGciOiJIUzI1NiIs..."
curl -s -H "Authorization: $TOKEN_A" \
  "https://api.target.example.com/api/v1/users/101/orders" | jq .
 
# Try accessing User B's resources with User A's token
curl -s -H "Authorization: $TOKEN_A" \
  "https://api.target.example.com/api/v1/users/102/orders" | jq .
 
# Fuzz object IDs with Burp Intruder or ffuf
ffuf -u "https://api.target.example.com/api/v1/orders/FUZZ" \
  -w <(seq 1 1000) \
  -H "Authorization: $TOKEN_A" \
  -mc 200 -t 10 -rate 50
 
# Test IDOR with different ID formats
# Numeric: /users/102
# UUID: /users/550e8400-e29b-41d4-a716-446655440000
# Encoded: /users/MTAy (base64)

Step 3: Test API2 - Broken Authentication

Assess authentication mechanisms for weaknesses.

# Test for missing authentication
curl -s "https://api.target.example.com/api/v1/users" | jq .
 
# Test JWT token vulnerabilities
# Decode JWT without verification
echo "eyJhbGciOiJIUzI1NiIs..." | cut -d. -f2 | base64 -d 2>/dev/null | jq .
 
# Test "alg: none" attack
# Header: {"alg":"none","typ":"JWT"}
# Create unsigned token with modified claims
 
# Test brute-force protection on login
ffuf -u "https://api.target.example.com/api/v1/auth/login" \
  -X POST -H "Content-Type: application/json" \
  -d '{"email":"admin@target.com","password":"FUZZ"}' \
  -w /usr/share/seclists/Passwords/Common-Credentials/top-1000.txt \
  -mc 200 -t 5 -rate 10
 
# Test password reset flow
curl -s -X POST "https://api.target.example.com/api/v1/auth/reset" \
  -H "Content-Type: application/json" \
  -d '{"email":"victim@target.com"}'
 
# Check if token is in response body instead of email only

Step 4: Test API3 - Broken Object Property Level Authorization

Test for excessive data exposure and mass assignment vulnerabilities.

# Check for excessive data in responses
curl -s -H "Authorization: $TOKEN_A" \
  "https://api.target.example.com/api/v1/users/101" | jq .
# Look for: password hashes, SSNs, internal IDs, admin flags, PII
 
# Test mass assignment - try adding admin properties
curl -s -X PUT \
  -H "Authorization: $TOKEN_A" \
  -H "Content-Type: application/json" \
  -d '{"name":"Test User","role":"admin","is_admin":true}' \
  "https://api.target.example.com/api/v1/users/101" | jq .
 
# Test with PATCH method
curl -s -X PATCH \
  -H "Authorization: $TOKEN_A" \
  -H "Content-Type: application/json" \
  -d '{"role":"admin","balance":999999}' \
  "https://api.target.example.com/api/v1/users/101" | jq .
 
# Check if filtering parameters expose more data
curl -s -H "Authorization: $TOKEN_A" \
  "https://api.target.example.com/api/v1/users/101?fields=all" | jq .
curl -s -H "Authorization: $TOKEN_A" \
  "https://api.target.example.com/api/v1/users/101?include=password,ssn" | jq .

Step 5: Test API4/API6 - Rate Limiting and Unrestricted Access to Sensitive Flows

Verify rate limiting and resource consumption controls.

# Test rate limiting on authentication endpoint
for i in $(seq 1 100); do
  status=$(curl -s -o /dev/null -w "%{http_code}" \
    -X POST -H "Content-Type: application/json" \
    -d '{"email":"test@test.com","password":"wrong"}' \
    "https://api.target.example.com/api/v1/auth/login")
  echo "Attempt $i: $status"
  if [ "$status" == "429" ]; then
    echo "Rate limited at attempt $i"
    break
  fi
done
 
# Test for unrestricted resource consumption
# Large pagination
curl -s -H "Authorization: $TOKEN_A" \
  "https://api.target.example.com/api/v1/users?limit=100000&offset=0" | jq '. | length'
 
# GraphQL depth/complexity attack
curl -s -X POST \
  -H "Content-Type: application/json" \
  -H "Authorization: $TOKEN_A" \
  -d '{"query":"{ users { friends { friends { friends { friends { name } } } } } }"}' \
  "https://api.target.example.com/graphql"
 
# Test SMS/email flooding via OTP endpoint
for i in $(seq 1 20); do
  curl -s -X POST -H "Content-Type: application/json" \
    -d '{"phone":"+1234567890"}' \
    "https://api.target.example.com/api/v1/auth/send-otp"
done

Step 6: Test API5 - Broken Function Level Authorization

Check for privilege escalation through administrative endpoints.

# Test admin endpoints with regular user token
ADMIN_ENDPOINTS=(
  "/api/v1/admin/users"
  "/api/v1/admin/settings"
  "/api/v1/admin/logs"
  "/api/v1/internal/config"
  "/api/v1/users?role=admin"
  "/api/v1/admin/export"
)
 
for endpoint in "${ADMIN_ENDPOINTS[@]}"; do
  for method in GET POST PUT DELETE; do
    status=$(curl -s -o /dev/null -w "%{http_code}" \
      -X "$method" \
      -H "Authorization: $TOKEN_A" \
      -H "Content-Type: application/json" \
      "https://api.target.example.com$endpoint")
    if [ "$status" != "403" ] && [ "$status" != "401" ] && [ "$status" != "404" ]; then
      echo "POTENTIAL ISSUE: $method $endpoint returned $status"
    fi
  done
done
 
# Test HTTP method switching
# If GET /admin/users returns 403, try:
curl -s -X POST -H "Authorization: $TOKEN_A" \
  "https://api.target.example.com/api/v1/admin/users"

Step 7: Test API7-API10 - SSRF, Misconfiguration, Inventory, and Unsafe Consumption

# API7: Server-Side Request Forgery
curl -s -X POST -H "Authorization: $TOKEN_A" \
  -H "Content-Type: application/json" \
  -d '{"url":"http://169.254.169.254/latest/meta-data/"}' \
  "https://api.target.example.com/api/v1/fetch-url"
 
curl -s -X POST -H "Authorization: $TOKEN_A" \
  -H "Content-Type: application/json" \
  -d '{"webhook_url":"http://127.0.0.1:6379/"}' \
  "https://api.target.example.com/api/v1/webhooks"
 
# API8: Security Misconfiguration
# Check CORS policy
curl -s -I -H "Origin: https://evil.example.com" \
  "https://api.target.example.com/api/v1/users" | grep -i "access-control"
 
# Check for verbose error messages
curl -s -X POST -H "Content-Type: application/json" \
  -d '{"invalid": "data' \
  "https://api.target.example.com/api/v1/users"
 
# Check security headers
curl -s -I "https://api.target.example.com/api/v1/health" | grep -iE \
  "(x-frame|x-content|strict-transport|content-security|x-xss)"
 
# API9: Improper Inventory Management
# Test deprecated API versions
for v in v0 v1 v2 v3; do
  curl -s -o /dev/null -w "$v: %{http_code}\n" \
    "https://api.target.example.com/api/$v/users"
done
 
# API10: Unsafe Consumption of APIs
# Test if the API blindly trusts third-party data
# Check webhook/callback implementations for injection

Key Concepts

Concept Description
BOLA (API1) Broken Object Level Authorization - accessing objects belonging to other users
Broken Authentication (API2) Weak authentication mechanisms allowing credential stuffing or token manipulation
BOPLA (API3) Broken Object Property Level Authorization - excessive data exposure or mass assignment
Unrestricted Resource Consumption (API4) Missing rate limiting enabling DoS or brute-force attacks
Broken Function Level Auth (API5) Regular users accessing admin-level API functions
SSRF (API7) Server-Side Request Forgery through API parameters accepting URLs
Security Misconfiguration (API8) Missing security headers, verbose errors, permissive CORS
Improper Inventory (API9) Undocumented, deprecated, or shadow API endpoints left exposed

Tools & Systems

Tool Purpose
Burp Suite Professional API interception, scanning, and manual testing
Postman API collection management and automated test execution
ffuf API endpoint and parameter fuzzing
Kiterunner API endpoint discovery using common API path patterns
jwt_tool JWT token analysis, manipulation, and attack automation
GraphQL Voyager GraphQL schema visualization and introspection analysis
Arjun HTTP parameter discovery for API endpoints

Common Scenarios

Scenario 1: BOLA in E-commerce API

User A can access User B's order details by changing the order ID in /api/v1/orders/{id}. The API only checks authentication but not authorization on the object level.

Scenario 2: Mass Assignment on User Profile

The user update endpoint accepts a role field in the JSON body. By adding "role":"admin" to a profile update request, a regular user escalates to administrator privileges.

Scenario 3: Deprecated API Version Bypass

The /api/v2/users endpoint has proper rate limiting, but /api/v1/users (still active) has no rate limiting. Attackers use the old version to brute-force credentials.

Scenario 4: GraphQL Introspection Data Leak

GraphQL introspection is enabled in production, exposing the entire schema including internal queries, mutations, and sensitive field names that are not used in the frontend.

Output Format

## API Security Assessment Report
 
**Target**: api.target.example.com
**API Type**: REST (OpenAPI 3.0)
**Assessment Date**: 2024-01-15
**OWASP API Security Top 10 (2023) Coverage**
 
| Risk | Status | Severity | Details |
|------|--------|----------|---------|
| API1: BOLA | VULNERABLE | Critical | /api/v1/orders/{id} - IDOR confirmed |
| API2: Broken Auth | VULNERABLE | High | No rate limit on /auth/login |
| API3: BOPLA | VULNERABLE | High | User role modifiable via mass assignment |
| API4: Resource Consumption | VULNERABLE | Medium | No pagination limit enforced |
| API5: Function Level Auth | PASS | - | Admin endpoints properly restricted |
| API6: Unrestricted Sensitive Flows | VULNERABLE | Medium | OTP endpoint lacks rate limiting |
| API7: SSRF | PASS | - | URL parameters properly validated |
| API8: Misconfiguration | VULNERABLE | Medium | Verbose stack traces in error responses |
| API9: Improper Inventory | VULNERABLE | Low | API v1 still accessible without docs |
| API10: Unsafe Consumption | NOT TESTED | - | No third-party API integrations found |
 
### Critical Finding: BOLA on Orders API
Authenticated users can access any order by iterating order IDs.
Tested range: 1-1000, 847 valid orders accessible.
PII exposure: names, addresses, payment details.
Source materials

References and resources

Everything below is rendered for inspection. Script files are read-only and never run.

References 1

api-reference.md2.3 KB

API Reference: Testing API Security with OWASP Top 10

requests Library

Installation

pip install requests

Key Methods

Method Description
requests.get(url, headers=, params=, timeout=) Send GET request
requests.post(url, json=, headers=, timeout=) Send POST request
requests.put(url, json=, headers=) Send PUT request
requests.patch(url, json=, headers=) Send PATCH request
requests.delete(url, headers=) Send DELETE request
requests.options(url, headers=) Send OPTIONS preflight

Response Object

Attribute Description
resp.status_code HTTP status code (200, 401, 403, 429)
resp.headers Response headers dict
resp.json() Parse response body as JSON
resp.text Response body as string
resp.elapsed Response time as timedelta

OWASP API Security Top 10 (2023)

ID Risk Test Approach
API1 Broken Object Level Auth Iterate object IDs with another user's token
API2 Broken Authentication Brute-force login, test JWT weaknesses
API3 Broken Object Property Level Auth Check excessive data + mass assignment
API4 Unrestricted Resource Consumption Test pagination limits, rate limiting
API5 Broken Function Level Auth Access admin endpoints as regular user
API6 Unrestricted Access to Sensitive Flows Abuse OTP, reset, registration flows
API7 Server-Side Request Forgery Inject internal URLs in URL parameters
API8 Security Misconfiguration Check headers, CORS, error verbosity
API9 Improper Inventory Management Find deprecated API versions
API10 Unsafe Consumption of APIs Test trust boundaries with third-party data

Security Header Checks

Header Expected Value
Strict-Transport-Security max-age=31536000; includeSubDomains
X-Content-Type-Options nosniff
X-Frame-Options DENY or SAMEORIGIN
Content-Security-Policy Restrictive policy

References

Scripts 1

agent.py10.3 KB
Display-only source. This catalog never executes bundled scripts.
#!/usr/bin/env python3
"""Agent for automated API security testing against OWASP API Security Top 10."""

import os
import requests
import json
import argparse
import urllib3
from datetime import datetime
from urllib.parse import urljoin

urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)


def test_bola(base_url, token, endpoints, id_range=(1, 20)):
    """Test for Broken Object Level Authorization (API1)."""
    print("\n[*] Testing API1: Broken Object Level Authorization (BOLA)...")
    findings = []
    headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
    for endpoint in endpoints:
        for obj_id in range(id_range[0], id_range[1]):
            url = urljoin(base_url, endpoint.replace("{id}", str(obj_id)))
            try:
                resp = requests.get(url, headers=headers, timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true")  # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
                if resp.status_code == 200 and len(resp.text) > 50:
                    findings.append({
                        "risk": "API1-BOLA", "url": url, "status": resp.status_code,
                        "body_length": len(resp.text), "severity": "CRITICAL",
                    })
                    print(f"  [!] VULNERABLE: GET {url} -> {resp.status_code} ({len(resp.text)} bytes)")
            except requests.RequestException:
                continue
    return findings


def test_broken_auth(base_url, login_endpoint="/api/v1/auth/login", attempts=50):
    """Test for Broken Authentication (API2) - rate limiting on login."""
    print("\n[*] Testing API2: Broken Authentication (rate limiting)...")
    url = urljoin(base_url, login_endpoint)
    findings = []
    rate_limited = False
    for i in range(1, attempts + 1):
        try:
            resp = requests.post(url, json={"email": "test@test.com", "password": f"wrong{i}"},
                                 timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true")  # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
            if resp.status_code == 429:
                print(f"  [+] Rate limited at attempt {i}")
                rate_limited = True
                break
        except requests.RequestException:
            break
    if not rate_limited:
        findings.append({
            "risk": "API2-BROKEN_AUTH", "url": url, "severity": "HIGH",
            "detail": f"No rate limiting after {attempts} failed login attempts",
        })
        print(f"  [!] No rate limiting after {attempts} attempts")
    return findings


def test_data_exposure(base_url, token, endpoints):
    """Test for Broken Object Property Level Authorization (API3)."""
    print("\n[*] Testing API3: Excessive Data Exposure...")
    headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
    sensitive_fields = ["password", "password_hash", "ssn", "credit_card", "secret",
                        "api_key", "token", "internal_id", "salt"]
    findings = []
    for endpoint in endpoints:
        url = urljoin(base_url, endpoint)
        try:
            resp = requests.get(url, headers=headers, timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true")  # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
            if resp.status_code == 200:
                try:
                    data = resp.json()
                    data_str = json.dumps(data).lower()
                    exposed = [f for f in sensitive_fields if f in data_str]
                    if exposed:
                        findings.append({
                            "risk": "API3-DATA_EXPOSURE", "url": url,
                            "exposed_fields": exposed, "severity": "HIGH",
                        })
                        print(f"  [!] {url}: Exposes {exposed}")
                except json.JSONDecodeError:
                    pass
        except requests.RequestException:
            continue
    return findings


def test_mass_assignment(base_url, token, endpoint, payload_extras):
    """Test for mass assignment vulnerabilities."""
    print("\n[*] Testing API3: Mass Assignment...")
    headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}
    url = urljoin(base_url, endpoint)
    findings = []
    for field, value in payload_extras.items():
        try:
            resp = requests.patch(url, headers=headers, json={field: value},
                                  timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true")  # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
            if resp.status_code in (200, 201):
                resp_data = resp.json() if resp.text else {}
                if str(value) in json.dumps(resp_data):
                    findings.append({
                        "risk": "API3-MASS_ASSIGNMENT", "url": url,
                        "field": field, "value": value, "severity": "CRITICAL",
                    })
                    print(f"  [!] VULNERABLE: Field '{field}' accepted with value '{value}'")
        except requests.RequestException:
            continue
    return findings


def test_security_headers(base_url):
    """Test for Security Misconfiguration (API8)."""
    print("\n[*] Testing API8: Security Misconfiguration (headers)...")
    findings = []
    try:
        resp = requests.get(base_url, timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true")  # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
        required_headers = {
            "Strict-Transport-Security": "HSTS",
            "X-Content-Type-Options": "nosniff",
            "X-Frame-Options": "clickjacking protection",
            "Content-Security-Policy": "CSP",
        }
        for header, desc in required_headers.items():
            if header.lower() not in {k.lower(): v for k, v in resp.headers.items()}:
                findings.append({
                    "risk": "API8-MISCONFIGURATION", "header": header,
                    "detail": f"Missing {desc}", "severity": "MEDIUM",
                })
                print(f"  [!] Missing: {header} ({desc})")
            else:
                print(f"  [+] Present: {header}")
    except requests.RequestException as e:
        print(f"  [-] Error: {e}")
    return findings


def test_cors(base_url, endpoints):
    """Test CORS configuration on API endpoints."""
    print("\n[*] Testing CORS configuration...")
    findings = []
    evil_origins = ["https://evil.com", "null", "http://localhost"]
    for endpoint in endpoints[:3]:
        url = urljoin(base_url, endpoint)
        for origin in evil_origins:
            try:
                resp = requests.get(url, headers={"Origin": origin}, timeout=10, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true")  # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
                acao = resp.headers.get("Access-Control-Allow-Origin", "")
                acac = resp.headers.get("Access-Control-Allow-Credentials", "")
                if acao == origin and acac.lower() == "true":
                    findings.append({
                        "risk": "CORS_MISCONFIGURATION", "url": url,
                        "origin": origin, "severity": "HIGH",
                    })
                    print(f"  [!] {url}: Reflects origin '{origin}' with credentials")
            except requests.RequestException:
                continue
    return findings


def test_api_versions(base_url, path_prefix="/api"):
    """Test for Improper Inventory Management (API9)."""
    print("\n[*] Testing API9: Improper Inventory Management...")
    findings = []
    versions = ["v0", "v1", "v2", "v3", "v4", "beta", "internal", "admin", "debug"]
    for v in versions:
        url = urljoin(base_url, f"{path_prefix}/{v}/users")
        try:
            resp = requests.get(url, timeout=5, verify=not os.environ.get("SKIP_TLS_VERIFY", "").lower() == "true")  # Set SKIP_TLS_VERIFY=true for self-signed certs in lab environments
            if resp.status_code not in (404, 000):
                findings.append({"risk": "API9-INVENTORY", "url": url, "status": resp.status_code})
                print(f"  [+] {v}: {resp.status_code}")
        except requests.RequestException:
            continue
    return findings


def generate_report(all_findings, output_path):
    """Generate OWASP API Security assessment report."""
    report = {
        "assessment_date": datetime.now().isoformat(),
        "total_findings": len(all_findings),
        "by_severity": {},
        "findings": all_findings,
    }
    for f in all_findings:
        sev = f.get("severity", "INFO")
        report["by_severity"][sev] = report["by_severity"].get(sev, 0) + 1
    with open(output_path, "w") as fh:
        json.dump(report, fh, indent=2)
    print(f"\n[*] Report saved to {output_path}")
    print(f"[*] Total findings: {len(all_findings)}")
    for sev, count in report["by_severity"].items():
        print(f"  {sev}: {count}")


def main():
    parser = argparse.ArgumentParser(description="OWASP API Security Top 10 Testing Agent")
    parser.add_argument("base_url", help="Base URL of the API (e.g., https://api.target.com)")
    parser.add_argument("--token", help="Bearer token for authentication")
    parser.add_argument("--endpoints", nargs="+", default=["/api/v1/users/{id}", "/api/v1/orders/{id}"])
    parser.add_argument("--login-endpoint", default="/api/v1/auth/login")
    parser.add_argument("-o", "--output", default="api_security_report.json")
    args = parser.parse_args()

    print(f"[*] OWASP API Security Top 10 Assessment")
    print(f"[*] Target: {args.base_url}")
    all_findings = []
    all_findings.extend(test_security_headers(args.base_url))
    all_findings.extend(test_cors(args.base_url, args.endpoints))
    all_findings.extend(test_api_versions(args.base_url))
    all_findings.extend(test_broken_auth(args.base_url, args.login_endpoint))
    if args.token:
        all_findings.extend(test_bola(args.base_url, args.token, args.endpoints))
        all_findings.extend(test_data_exposure(args.base_url, args.token, args.endpoints))
        all_findings.extend(test_mass_assignment(args.base_url, args.token, args.endpoints[0],
                                                  {"role": "admin", "is_admin": True}))
    generate_report(all_findings, args.output)


if __name__ == "__main__":
    main()
Keep exploring