The vendored SKILL.md files were byte-identical to upstream, but the playbooks, templates and schemas they reference were left behind in the original repos, so a loading skill pointed at a file that does not exist anywhere in this repository. Vendored 1:1 into each skill directory (so they survive `cp -r skills/* ~/.hermes/skills/`): - plays/ for api-security-review, code-review-security, sca-audit, secrets-scan, web-security-review (OWASP/secure-agent-playbook) - templates/finding.md for the four OWASP skills that cite it - schemas/finding.schema.json for cve-triage, patch-prioritization, dependency-scanning (UnitOneAI/SecuritySkills) - docs/sarif-output.md, docs/fixer-policy.md and docs/normalized-json-output.md for dependency-scanning Path fixes, the only edit to vendored text: - the three UnitOneAI skills referenced ../../../schemas/ and ../../../docs/ which assumed upstream's skills/<category>/<skill>/ depth; this repo is flat, so the links now resolve inside the skill directory - safedeps told the agent to run <skill_dir>/../../check_deps.py while the script is vendored next to SKILL.md; corrected to <skill_dir>/check_deps.py and documented the script's exit codes (1 = finding, not a crash) - NOTICE.md records the new files, the path edits, and the remaining known gaps (hermaguard's kensei-pipeline references, the ASVS dataset citations, and hermaguard being behind upstream) Verified: every backticked path in every SKILL.md now resolves; check_deps.py runs from the installed location and returns live OSV.dev data ([email protected] -> 2 advisories); hermaguard's 46 tests still pass.
39 KiB
Play: API Security Review
Comprehensive API security assessment against OWASP API Security Top 10 (2023) with real-world attack scenarios, automated testing integration, and detailed remediation guidance.
Trigger Conditions
Use this play when:
- Reviewing OpenAPI/Swagger specifications or API documentation
- Auditing REST, GraphQL, gRPC, or SOAP API implementations
- Testing API authentication and authorization mechanisms
- Reviewing API gateway configurations (Kong, nginx, Envoy, AWS API Gateway)
- Conducting pre-deployment security assessments
- Performing API penetration testing or security audits
- A user asks to "test my API for security issues"
Inputs
- OpenAPI/Swagger specification (YAML/JSON) or API documentation
- API source code (routes, controllers, middleware, authentication logic)
- API gateway and infrastructure configurations
- Authentication implementation details (JWT, OAuth, API keys)
- Environment information (production, staging, development)
- Previous security assessment reports (if available)
Procedure
Phase 1: Discovery & Reconnaissance
1.1 API Surface Enumeration
With OpenAPI Spec:
# Parse and analyze OpenAPI spec
# Look for:
- Total endpoints and HTTP methods
- Authentication requirements per endpoint
- Request/response schemas
- Rate limiting documentation
- Deprecated endpoints
Without OpenAPI Spec (Code Analysis):
- Scan route definitions in framework-specific patterns:
- Express:
app.get(),router.post() - Django:
path(),url() - Rails:
resources,get/post/put/delete - Spring:
@GetMapping,@RequestMapping - FastAPI:
@app.get(), decorator-based routes
- Express:
Key Questions:
- How many endpoints exist?
- Which require authentication vs. public?
- What data types are exchanged?
- Are there file upload endpoints?
- Are there admin/debug endpoints?
1.2 Authentication Mechanism Identification
Document all auth mechanisms in use:
| Mechanism | Indicators | Security Considerations |
|---|---|---|
| JWT | Authorization: Bearer eyJ... |
Algorithm, signing key strength, expiration |
| OAuth 2.0 | /oauth/authorize, /oauth/token |
Flow type, PKCE, state parameter, redirect URI |
| API Keys | X-API-Key, api_key query param |
Key rotation, transmission security, scope |
| Session Cookies | Set-Cookie: sessionid=... |
HttpOnly, Secure, SameSite, expiration |
| mTLS | Client certificate requirements | Certificate validation, revocation |
| Basic Auth | Authorization: Basic base64 |
HTTPS enforcement, credential storage |
Phase 2: Authentication Security Assessment
2.1 JWT Security Testing
Common JWT Vulnerabilities:
-
Algorithm Confusion (CVE-2015-9235)
Attack: Change algorithm from RS256 to none or HS256 Header: {"alg":"none","typ":"JWT"} Result: Token accepted without verification -
Weak Signing Keys
# Test for weak secrets if using HS256 # Use jwt_tool or hashcat for brute force jwt_tool.py token_here -C -d /usr/share/wordlists/rockyou.txt -
Missing Expiration
Check: Does payload contain "exp" claim? Risk: Tokens valid forever if stolen -
Token Storage Issues
// Insecure: localStorage (XSS accessible) localStorage.setItem('jwt', token); // More secure: httpOnly cookie document.cookie = "jwt=" + token + "; HttpOnly; Secure; SameSite=Strict";
Testing Checklist:
- Test algorithm confusion (change alg to "none")
- Attempt key confusion attacks (RS256 → HS256)
- Check for missing expiration (exp claim)
- Verify signature validation cannot be bypassed
- Test token revocation mechanisms
- Check for sensitive data in JWT payload
2.2 OAuth 2.0 Security Testing
Critical Checks:
-
Authorization Code Flow
Required: PKCE (Proof Key for Code Exchange) Check: code_challenge and code_verifier parameters Risk: Without PKCE, auth codes can be stolen and used -
State Parameter
Required: Random state parameter to prevent CSRF Check: Is state validated on callback? Attack: Omit state → CSRF login attack -
Redirect URI Validation
Check: Is redirect_uri strictly validated? Attack: redirect_uri=https://attacker.com/callback Risk: Authorization code/token sent to attacker -
Scope Validation
Check: Can user modify requested scopes? Attack: Change scope=read to scope=admin Risk: Privilege escalation via scope manipulation
Testing Checklist:
- Verify PKCE implementation for public clients
- Test state parameter CSRF protection
- Attempt redirect URI manipulation
- Test scope escalation attacks
- Check for open redirect vulnerabilities
- Verify token endpoint authentication
2.3 API Key Security
Common Issues:
-
Key Exposure
# Search for hardcoded keys grep -r "api_key\|apikey\|api-key" --include="*.js" --include="*.py" --include="*.env" # Check client-side code # API keys in frontend code can be stolen -
Insufficient Scope
Issue: Single key with full access Best Practice: Scoped keys (read-only, write, admin) -
Missing Rotation
Check: Can keys be rotated without downtime? Risk: Compromised keys valid indefinitely
Phase 3: OWASP API Top 10 Deep Assessment
API1: Broken Object Level Authorization (BOLA/IDOR)
Risk: Attackers access other users' data by manipulating object IDs
Real-World Impact:
- 2019: First American Financial exposed 885 million records via IDOR
- 2020: Twitter API IDOR allowed viewing protected tweets
- 2021: Facebook IDOR exposed private posts
Attack Scenarios:
-
Sequential ID Manipulation
GET /api/users/123/orders # Try: GET /api/users/124/orders # Access other user's orders GET /api/users/1/orders # Access admin's orders -
UUID Predictability
If using predictable UUIDv1 (timestamp-based): Generate nearby UUIDs to find other users -
Batch Endpoint Bypass
POST /api/orders/bulk { "order_ids": [1001, 1002, 1003, 1004] # Can access any IDs? } -
Indirect Object References
GET /api/orders?id=1001 # vs GET /api/orders?id=1001&user_id=123 # Is user_id validated?
Testing Approach:
- Map Object IDs: Identify all endpoints accepting object IDs
- Cross-User Testing:
- Create User A and User B accounts
- Have User A create resources
- Attempt to access with User B's credentials
- Batch Testing: Test if batch endpoints filter by ownership
- File Access: Test file download endpoints with different IDs
- IDOR Automation:
# Use Burp Intruder or custom script # Iterate through IDs and check for 200 responses for i in {1..1000}; do curl -H "Authorization: Bearer $TOKEN" \ "https://api.example.com/orders/$i" | grep -q "order_id" && echo "Found: $i" done
Remediation:
# BAD: No ownership check
def get_order(order_id):
return Order.objects.get(id=order_id)
# GOOD: Verify ownership
def get_order(order_id, user):
return Order.objects.get(id=order_id, user=user)
# Or: Check ownership after retrieval
order = Order.objects.get(id=order_id)
if order.user_id != user.id:
raise PermissionDenied()
return order
API2: Broken Authentication
Risk: Weak authentication allows account takeover
Real-World Impact:
- 2022: Uber breach via compromised contractor account
- 2023: 23andMe breach via credential stuffing
Attack Scenarios:
-
Credential Stuffing
# Automated login attempts with breached credentials # APIs often lack CAPTCHA or rate limiting -
JWT None Algorithm
Header: {"alg":"none","typ":"JWT"} Payload: {"user_id":1,"role":"admin"} Signature: (empty) Result: Server accepts unsigned token -
Weak Password Policy
POST /api/login {"username":"admin","password":"123456"} # Test common passwords # Check minimum length requirements -
Session Fixation
Attacker sets session ID Victim logs in with attacker's session ID Attacker uses same session ID to access account
Testing Approach:
-
Brute Force Testing:
# Test login endpoint rate limiting for i in {1..100}; do curl -X POST https://api.example.com/login \ -d "username=admin&password=wrong$i" done # Check if account locks or IP is blocked -
Token Security:
- Decode JWT and analyze claims
- Test algorithm confusion
- Check expiration and refresh mechanisms
- Test token revocation
-
Password Policy:
- Test minimum length (should be 12+)
- Test complexity requirements
- Check common password blocking
- Verify secure password reset flow
-
Multi-Factor Authentication:
- Is MFA enforced for sensitive operations?
- Can MFA be bypassed?
- Are backup codes secure?
Remediation:
# Implement rate limiting
from django_ratelimit.decorators import ratelimit
@ratelimit(key='ip', rate='5/m', method='POST')
def login(request):
# Login logic
pass
# Strong password validation
from django.contrib.auth.password_validation import validate_password
def validate_user_password(password):
try:
validate_password(password)
except ValidationError as e:
raise e
# Secure JWT configuration
JWT_SETTINGS = {
'ALGORITHM': 'HS256', # Use strong algorithms only
'VERIFY_EXPIRATION': True,
'EXPIRATION_DELTA': timedelta(minutes=15),
'REFRESH_EXPIRATION_DELTA': timedelta(days=7),
}
API3: Broken Object Property Level Authorization
Risk: Mass assignment and data over-exposure
Real-World Impact:
- 2012: GitHub mass assignment allowed unauthorized repo access
- 2019: Facebook mass assignment exposed user data
Attack Scenarios:
-
Mass Assignment
POST /api/users { "username": "newuser", "email": "[email protected]", "password": "password123", "role": "admin", # Should not be user-settable "is_verified": true # Should not be user-settable } -
Response Over-Exposure
GET /api/users/123 Response: { "id": 123, "username": "john", "email": "[email protected]", "password_hash": "$2b$12$...", # Should not be exposed! "ssn": "123-45-6789", # PII exposure "internal_notes": "Difficult customer" } -
Field-Level Authorization Bypass
PUT /api/users/123 { "salary": 200000 # Can regular users modify this? }
Testing Approach:
-
Mass Assignment Testing:
# Send extra fields in POST/PUT requests payload = { "username": "test", "email": "[email protected]", "password": "password", "role": "admin", # Try to escalate "is_admin": True, "credit_balance": 10000 } -
Response Analysis:
- Capture all API responses
- Look for sensitive fields (passwords, tokens, PII)
- Compare responses for different user roles
- Check if internal fields are exposed
-
GraphQL Over-Exposure:
# Request fields you shouldn't see query { user(id: 123) { id name email password # Should be rejected ssn # Should be rejected salary # Should be rejected for non-admins } }
Remediation:
# Use allowlist approach
ALLOWED_USER_FIELDS = ['username', 'email', 'password']
def update_user(user_id, data, current_user):
# Filter input to allowed fields only
filtered_data = {k: v for k, v in data.items()
if k in ALLOWED_USER_FIELDS}
# Check field-level permissions
if 'role' in data and not current_user.is_admin:
raise PermissionDenied("Cannot modify role")
return User.objects.filter(id=user_id).update(**filtered_data)
# Response serialization
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email'] # Explicit allowlist
# Never include: password, ssn, internal_notes
API4: Unrestricted Resource Consumption
Risk: DoS via resource exhaustion, financial drain
Real-World Impact:
- 2023: OpenAI API abuse cost companies thousands
- 2022: GraphQL query DoS attacks on multiple platforms
Attack Scenarios:
-
Rate Limit Bypass
# Distributed attack from multiple IPs # Or using different API keys # Or changing User-Agent headers -
Expensive Query Abuse
# Deeply nested GraphQL query query { users { posts { comments { author { posts { comments { author { posts { ... } # Infinite recursion potential } } } } } } } } -
Large Payload Attacks
POST /api/upload Content-Length: 1000000000 # 1GB file # Or JSON bomb: {"a":{"a":{"a":{"a":...}}}} # Deeply nested JSON -
Pagination Abuse
GET /api/items?limit=1000000 # Request all records GET /api/items?page=1&per_page=10000 # Large page size
Testing Approach:
-
Rate Limit Testing:
# Test from single IP for i in {1..1000}; do curl https://api.example.com/data done # Test bypass methods: # - Different API keys # - Rotating User-Agents # - Distributed requests -
GraphQL DoS:
# Test query complexity # Test query depth # Test expensive operations -
Resource Exhaustion:
- Upload extremely large files
- Send deeply nested JSON
- Request massive result sets
- Trigger expensive database queries
Remediation:
# Rate limiting
from django_ratelimit.decorators import ratelimit
@ratelimit(key='user', rate='100/h', method=['GET', 'POST'])
def api_endpoint(request):
pass
# GraphQL safeguards
GRAPHQL_SETTINGS = {
'MAX_DEPTH': 10,
'MAX_COMPLEXITY': 1000,
'DISABLE_INTROSPECTION': True, # In production
}
# Pagination limits
MAX_PAGE_SIZE = 100
DEFAULT_PAGE_SIZE = 20
def get_items(request):
page_size = min(
int(request.GET.get('limit', DEFAULT_PAGE_SIZE)),
MAX_PAGE_SIZE
)
return Item.objects.all()[:page_size]
# File upload limits
MAX_UPLOAD_SIZE = 10 * 1024 * 1024 # 10MB
# Timeout configuration
REQUEST_TIMEOUT = 30 # seconds
API5: Broken Function Level Authorization
Risk: Access to admin/sensitive functions without proper authorization
Real-World Impact:
- 2020: Twitter admin tool breach
- 2021: Parler admin API exposed
Attack Scenarios:
-
Admin Endpoint Discovery
GET /api/admin/users # Try common admin paths GET /api/v1/admin/config GET /api/internal/audit-logs GET /api/debug/info -
HTTP Method Switching
GET /api/users/123 # Allowed for regular users DELETE /api/users/123 # Should require admin, but is it checked? PUT /api/users/123/role # Can users change their own role? -
Parameter-Based Access Control
POST /api/users { "username": "test", "role": "admin" # Is this validated server-side? }
Testing Approach:
-
Endpoint Enumeration:
# Use wordlists to find admin endpoints # Common paths: /admin, /internal, /debug, /system -
Method Testing:
# For each endpoint, test all HTTP methods curl -X GET /api/resource curl -X POST /api/resource curl -X PUT /api/resource curl -X DELETE /api/resource curl -X PATCH /api/resource -
Role Escalation:
- Create regular user account
- Attempt to access admin endpoints
- Try to modify role/privilege fields
Remediation:
# Centralized authorization decorator
from functools import wraps
def require_admin(f):
@wraps(f)
def decorated_function(request, *args, **kwargs):
if not request.user.is_admin:
return Response(
{"error": "Admin access required"},
status=403
)
return f(request, *args, **kwargs)
return decorated_function
@require_admin
def admin_endpoint(request):
pass
# Middleware approach
class AdminAuthorizationMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.admin_paths = ['/api/admin/', '/api/internal/']
def __call__(self, request):
if any(request.path.startswith(path) for path in self.admin_paths):
if not request.user.is_admin:
return HttpResponseForbidden()
return self.get_response(request)
API6: Unrestricted Access to Sensitive Business Flows
Risk: Automated abuse of business logic
Real-World Impact:
- Ticket scalping: Bots buying all concert tickets
- E-commerce: Inventory hoarding, price scraping
- Crypto: Flash loan attacks via API manipulation
Attack Scenarios:
-
Inventory Exhaustion
POST /api/cart/add {"product_id": 123, "quantity": 999999} # Buy all inventory -
Automated Purchasing
# Script that completes purchase faster than humans # Bypassing any client-side rate limits -
Price Manipulation
POST /api/order { "items": [...], "total": 1.00 # Is total calculated server-side? } -
Reservation Abuse
POST /api/reservations {"resource_id": 123, "start": "2024-01-01", "end": "2025-12-31"} # Reserve resource for entire year
Testing Approach:
-
Business Logic Testing:
- Attempt to complete purchases with modified prices
- Try to add negative quantities
- Test race conditions in multi-step flows
- Attempt to bypass payment steps
-
Automation Testing:
# Script to test if flows can be automated import requests import time start = time.time() for i in range(100): requests.post('/api/purchase', json={...}) elapsed = time.time() - start # If no rate limiting, 100 requests complete quickly
Remediation:
# Server-side price calculation
def create_order(items, user):
total = sum(item.product.price * item.quantity for item in items)
# Never trust client-provided total
# Inventory checks
for item in items:
if item.quantity > item.product.available_stock:
raise ValidationError("Insufficient inventory")
# Rate limiting per user
if user.orders.filter(created_at__gte=timezone.now() - timedelta(hours=1)).count() > 10:
raise RateLimitError("Too many orders")
return Order.objects.create(user=user, items=items, total=total)
# CAPTCHA for sensitive operations
from django_recaptcha.fields import ReCaptchaField
class PurchaseForm(forms.Form):
recaptcha = ReCaptchaField()
# ... other fields
API7: Server-Side Request Forgery (SSRF)
Risk: Server makes requests to attacker-controlled or internal resources
Real-World Impact:
- 2019: Capital One breach via SSRF (100M records)
- 2021: Multiple cloud metadata service exploits
Attack Scenarios:
-
Cloud Metadata Access
GET /api/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ # Retrieves AWS credentials -
Internal Service Access
GET /api/fetch?url=http://localhost:8080/admin GET /api/fetch?url=http://internal-api.company.local/secrets -
URL Bypass Techniques
http://127.0.0.1 → localhost bypass http://0177.0.0.1 → Octal encoding http://2130706433 → Decimal IP http://0x7f.0x0.0x0.0x1 → Hex encoding http://[::ffff:127.0.0.1] → IPv6 http://127.1 → Shortened -
DNS Rebinding
Attacker controls DNS that resolves to: - First request: legitimate.com → 1.2.3.4 - Second request: attacker.com → 169.254.169.254
Testing Approach:
-
Basic SSRF Testing:
# Test internal IP access curl "https://api.example.com/fetch?url=http://169.254.169.254/" curl "https://api.example.com/fetch?url=http://localhost:22" curl "https://api.example.com/fetch?url=file:///etc/passwd" -
Bypass Testing:
# Try various encodings curl "https://api.example.com/fetch?url=http://0177.0.0.1/" curl "https://api.example.com/fetch?url=http://2130706433/" curl "https://api.example.com/fetch?url=http://[::ffff:127.0.0.1]/" -
Port Scanning via SSRF:
# Test if server can reach internal ports for port in 22 80 443 3306 5432 6379 8080; do curl "https://api.example.com/fetch?url=http://localhost:$port" done
Remediation:
import ipaddress
import re
from urllib.parse import urlparse
def is_internal_ip(ip_str):
"""Check if IP is internal/private"""
try:
ip = ipaddress.ip_address(ip_str)
return ip.is_private or ip.is_loopback or ip.is_reserved
except ValueError:
return False
def validate_url(url):
"""Validate URL for SSRF prevention"""
parsed = urlparse(url)
# Allowlist approach (recommended)
allowed_hosts = ['api.trusted.com', 'cdn.trusted.com']
if parsed.hostname not in allowed_hosts:
raise ValidationError("URL not in allowlist")
# Blocklist approach (supplementary)
blocked_schemes = ['file', 'ftp', 'gopher', 'dict']
if parsed.scheme in blocked_schemes:
raise ValidationError("Scheme not allowed")
# Resolve and check IP
import socket
try:
ip = socket.gethostbyname(parsed.hostname)
if is_internal_ip(ip):
raise ValidationError("Internal IPs not allowed")
except socket.gaierror:
raise ValidationError("Could not resolve hostname")
return url
# Alternative: Use a dedicated SSRF protection library
# pip install ssrf-protect
from ssrf_protect import SSRFProtect
@SSRFProtect()
def fetch_url(request):
url = request.GET.get('url')
# Safe to fetch
return requests.get(url)
API8: Security Misconfiguration
Risk: Insecure default configurations expose APIs to attacks
Real-World Impact:
- 2018: Tesla Kubernetes API exposed (cryptomining)
- 2020: Multiple Elasticsearch instances exposed without auth
Attack Scenarios:
-
CORS Misconfiguration
Access-Control-Allow-Origin: * # Too permissive Access-Control-Allow-Credentials: true # Dangerous combination -
Verbose Error Messages
{ "error": "SQL syntax error near 'user_id = 123'", "stack_trace": "...", "query": "SELECT * FROM users WHERE user_id = 123" } -
Missing Security Headers
# Missing headers: X-Content-Type-Options: nosniff X-Frame-Options: DENY Content-Security-Policy: ... Strict-Transport-Security: max-age=31536000 -
Debug Mode Enabled
/api/debug/vars # Exposes Go runtime variables /api/swagger-ui/ # API documentation with try-it feature /api/actuator/health # Spring Boot endpoints
Testing Approach:
-
Security Headers Check:
curl -I https://api.example.com/endpoint | grep -i "access-control\|x-" -
Error Handling Test:
# Trigger errors and check responses curl "https://api.example.com/endpoint?param=' OR 1=1--" curl "https://api.example.com/endpoint" -X INVALID_METHOD -
Debug Endpoint Discovery:
# Common debug paths /debug /actuator /swagger-ui /api-docs /health /metrics
Remediation:
# Django middleware for security headers
class SecurityHeadersMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
response = self.get_response(request)
# Security headers
response['X-Content-Type-Options'] = 'nosniff'
response['X-Frame-Options'] = 'DENY'
response['X-XSS-Protection'] = '1; mode=block'
response['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
response['Content-Security-Policy'] = "default-src 'self'"
response['Referrer-Policy'] = 'strict-origin-when-cross-origin'
# CORS (restrictive)
response['Access-Control-Allow-Origin'] = 'https://trusted-domain.com'
response['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE'
response['Access-Control-Allow-Headers'] = 'Content-Type, Authorization'
return response
# Disable debug mode in production
DEBUG = False
# Custom error handler (don't expose internals)
from django.http import JsonResponse
def custom_error_handler(request, exception=None):
return JsonResponse(
{"error": "An error occurred"},
status=500
)
API9: Improper Inventory Management
Risk: Shadow APIs and undocumented endpoints create attack surface
Real-World Impact:
- 2021: Facebook API exposed user data via undocumented endpoint
- 2022: Twitter API v1.1 endpoints still accessible after v2 launch
Attack Scenarios:
-
Shadow API Discovery
Compare OpenAPI spec with actual code Look for endpoints not in documentation -
Deprecated API Usage
GET /api/v1/users # Old version still active GET /api/v2/users # New version # v1 may have known vulnerabilities -
Environment Confusion
Production API: api.company.com Staging API: api-staging.company.com # Exposed? Dev API: api-dev.company.com # Exposed with debug enabled?
Testing Approach:
-
API Inventory:
# Extract all routes from code grep -r "@app.route\|@GetMapping\|path(" --include="*.py" --include="*.java" # Compare with OpenAPI spec # Identify undocumented endpoints -
Version Testing:
# Test old API versions curl https://api.example.com/v1/users curl https://api.example.com/v2/users -
Environment Discovery:
# Check for exposed environments curl https://api-staging.example.com/health curl https://api-dev.example.com/debug
Remediation:
# API Gateway configuration (Kong example)
plugins:
- name: request-termination
config:
status_code: 410
message: "API version deprecated"
route: api-v1-routes
# Automated API discovery
# Use tools like:
# - OWASP ZAP API scanner
# - Postman collection generator
# - Swagger Codegen
# Regular audits
# Monthly: Compare deployed endpoints with documentation
# Quarterly: Review and deprecate old API versions
API10: Unsafe Consumption of APIs
Risk: Trusting third-party API responses leads to injection attacks
Real-World Impact:
- 2020: Multiple XXE attacks via external API responses
- 2021: Deserialization attacks from webhook payloads
Attack Scenarios:
-
XXE via External API
<!-- Malicious response from third-party API --> <?xml version="1.0"?> <!DOCTYPE foo [ <!ENTITY xxe SYSTEM "file:///etc/passwd"> ]> <data>&xxe;</data> -
Deserialization Attack
# Attacker controls serialized data import pickle malicious = b'...crafted pickle payload...' data = pickle.loads(malicious) # RCE! -
Webhook Replay
# Attacker captures webhook and replays it POST /api/webhooks/payment X-Webhook-Signature: valid_signature {"event": "payment.success", "amount": 1000} # Replay same request to double credits -
JSON Injection
// Malicious JSON from external API { "username": "admin", "bio": "<script>alert('XSS')</script>" }
Testing Approach:
-
XXE Testing:
# If API accepts XML from third parties # Send malicious XML to test for XXE -
Deserialization Testing:
# Check for unsafe deserialization # Look for pickle, ObjectInputStream, unserialize -
Webhook Security:
# Test webhook replay # Check signature validation # Verify timestamp/nonce checks
Remediation:
# XXE Prevention
import xml.etree.ElementTree as ET
from defusedxml import ElementTree as DefusedET
def parse_xml_safely(xml_string):
# Use defusedxml to prevent XXE
return DefusedET.fromstring(xml_string)
# Deserialization Safety
import json
def safe_deserialize(data):
# Use JSON instead of pickle for untrusted data
return json.loads(data)
# If you must use pickle, sign the data
import hmac
import hashlib
signature = hmac.new(
SECRET_KEY,
data,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected_signature):
raise ValueError("Invalid signature")
return pickle.loads(data)
# Webhook Security
import hmac
import hashlib
import time
def verify_webhook(request):
# Verify signature
signature = request.headers.get('X-Webhook-Signature')
expected = hmac.new(
WEBHOOK_SECRET,
request.body,
hashlib.sha256
).hexdigest()
if not hmac.compare_digest(signature, expected):
raise ValueError("Invalid signature")
# Check timestamp (prevent replay)
timestamp = int(request.headers.get('X-Webhook-Timestamp'))
if abs(time.time() - timestamp) > 300: # 5 minutes
raise ValueError("Request too old")
# Check nonce (prevent replay)
nonce = request.headers.get('X-Webhook-Nonce')
if nonce in used_nonces:
raise ValueError("Nonce already used")
used_nonces.add(nonce)
return True
Phase 4: Automated Security Testing
4.1 API Security Scanning
OWASP ZAP:
# Automated API scan
zap-api-scan.py -t https://api.example.com/openapi.json -f openapi
# With authentication
zap-api-scan.py -t https://api.example.com/openapi.json \
-f openapi \
-config "replacer.full_list(0).description=auth" \
-config "replacer.full_list(0).enabled=true" \
-config "replacer.full_list(0).matchtype=REQ_HEADER" \
-config "replacer.full_list(0).matchstr=Authorization" \
-config "replacer.full_list(0).replacement=Bearer $TOKEN"
Burp Suite:
- Import OpenAPI spec
- Run active scan on all endpoints
- Test for BOLA, authentication bypasses
Postman Security Tests:
// Test for sensitive data in response
pm.test("Response does not contain sensitive data", function () {
pm.expect(pm.response.text()).to.not.include("password");
pm.expect(pm.response.text()).to.not.include("secret");
});
// Test for security headers
pm.test("Security headers present", function () {
pm.response.to.have.header("X-Content-Type-Options");
pm.response.to.have.header("X-Frame-Options");
});
4.2 Custom Security Tests
BOLA Testing Script:
import requests
def test_bola(base_url, token, user_a_id, user_b_id):
"""Test for Broken Object Level Authorization"""
# Get User A's token
headers = {"Authorization": f"Bearer {token}"}
# Try to access User B's resources
endpoints = [
f"/api/users/{user_b_id}",
f"/api/users/{user_b_id}/orders",
f"/api/orders?user_id={user_b_id}",
]
for endpoint in endpoints:
response = requests.get(f"{base_url}{endpoint}", headers=headers)
if response.status_code == 200:
print(f"BOLA VULNERABILITY: {endpoint}")
print(f"User A can access User B's data!")
Rate Limit Testing:
import requests
import time
def test_rate_limiting(url, headers, requests_count=100):
"""Test if rate limiting is enforced"""
success_count = 0
start_time = time.time()
for i in range(requests_count):
response = requests.get(url, headers=headers)
if response.status_code == 200:
success_count += 1
elif response.status_code == 429:
print(f"Rate limit hit after {i} requests")
break
elapsed = time.time() - start_time
print(f"Made {success_count} requests in {elapsed:.2f} seconds")
if success_count == requests_count:
print("WARNING: No rate limiting detected!")
Phase 5: API Gateway & Infrastructure Review
5.1 Kong Gateway Configuration
# kong.yml
plugins:
# Rate limiting
- name: rate-limiting
config:
minute: 100
hour: 1000
policy: redis
redis_host: redis
# CORS (restrictive)
- name: cors
config:
origins:
- "https://app.example.com"
methods:
- GET
- POST
- PUT
- DELETE
headers:
- Authorization
- Content-Type
credentials: true
max_age: 3600
# Request size limiting
- name: request-size-limiting
config:
allowed_payload_size: 10 # MB
# Bot detection
- name: bot-detection
config:
deny:
- "Mozilla/5.0 (compatible; Googlebot/2.1)"
# IP restriction
- name: ip-restriction
config:
allow:
- "10.0.0.0/8"
- "172.16.0.0/12"
5.2 nginx Configuration
server {
listen 443 ssl http2;
server_name api.example.com;
# SSL configuration
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256';
# Security headers
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
# Rate limiting
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s;
limit_req zone=api burst=20 nodelay;
# Request size limit
client_max_body_size 10M;
# Timeout settings
proxy_connect_timeout 30s;
proxy_send_timeout 30s;
proxy_read_timeout 30s;
location / {
proxy_pass http://backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
5.3 AWS API Gateway
# serverless.yml or CloudFormation
Resources:
ApiGateway:
Type: AWS::ApiGateway::RestApi
Properties:
Name: SecureAPI
# Throttling
ApiGatewayStage:
Type: AWS::ApiGateway::Stage
Properties:
RestApiId: !Ref ApiGateway
StageName: prod
MethodSettings:
- ResourcePath: /*
HttpMethod: *
ThrottlingBurstLimit: 100
ThrottlingRateLimit: 50
# WAF
WebACL:
Type: AWS::WAFv2::WebACL
Properties:
Name: APIWAF
Rules:
- Name: RateLimit
Priority: 1
Statement:
RateBasedStatement:
Limit: 2000
AggregateKeyType: IP
Action:
Block: {}
VisibilityConfig:
SampledRequestsEnabled: true
Output Format
## API Security Assessment: [API Name]
### Executive Summary
- **API Type**: REST | GraphQL | gRPC | SOAP
- **Total Endpoints**: [count]
- **Authentication**: [JWT | OAuth 2.0 | API Keys | Session]
- **Risk Rating**: [Critical | High | Medium | Low]
- **Overall Security Posture**: [Brief assessment]
### API Surface Inventory
#### Authentication Mechanisms
| Mechanism | Implementation | Security Rating |
|-----------|---------------|-----------------|
| JWT | HS256, 15min expiry | ✅ Secure |
| OAuth 2.0 | PKCE enabled | ✅ Secure |
#### Endpoint Breakdown
| Category | Count | Auth Required |
|----------|-------|---------------|
| Public | 5 | No |
| Authenticated | 45 | Yes |
| Admin | 8 | Yes + Admin role |
### Risk Matrix
| OWASP API Ref | Risk | Severity | Status | Evidence |
|--------------|------|----------|--------|----------|
| API1 | BOLA | HIGH | Finding | /api/orders/{id} lacks ownership check |
| API2 | Broken Auth | MEDIUM | Finding | JWT alg=none accepted |
| API3 | BOPA | HIGH | Finding | Mass assignment on /api/users |
| API4 | Resource Consumption | LOW | Finding | No rate limiting on search |
| API5 | BFLA | MEDIUM | Finding | Admin endpoints accessible to users |
| API6 | Business Flows | N/A | N/A | Not applicable |
| API7 | SSRF | CRITICAL | Finding | Cloud metadata accessible |
| API8 | Misconfiguration | MEDIUM | Finding | Debug mode enabled |
| API9 | Inventory | LOW | Finding | Undocumented /api/internal endpoints |
| API10 | Unsafe Consumption | HIGH | Finding | Webhooks lack signature validation |
### Critical Findings
#### [CRITICAL] API7: SSRF - Cloud Metadata Exposure
- **Location**: `/api/fetch?url=` parameter
- **Impact**: AWS credentials can be extracted, leading to full cloud compromise
- **Evidence**:
GET /api/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/ Response: 200 OK with IAM role credentials
- **Remediation**: Implement URL allowlist, block internal IPs
### High Findings
[Additional findings...]
### Positive Security Controls
- ✅ JWT tokens use strong signing (RS256)
- ✅ HTTPS enforced with TLS 1.2+
- ✅ Security headers present
- ✅ Input validation on all endpoints
### Remediation Roadmap
#### Immediate (Critical/High - 1 week)
1. Fix SSRF vulnerability (API7)
2. Implement BOLA checks on all object endpoints (API1)
3. Disable debug mode (API8)
#### Short-term (High/Medium - 1 month)
4. Implement comprehensive rate limiting (API4)
5. Add field-level authorization (API3)
6. Secure admin endpoints (API5)
#### Long-term (Medium/Low - 3 months)
7. Implement API inventory management (API9)
8. Add webhook signature validation (API10)
9. Conduct regular security testing
### Testing Artifacts
- [ ] OWASP ZAP scan results
- [ ] Burp Suite scan results
- [ ] Custom test scripts output
- [ ] API gateway configuration review
### References
- OWASP API Security Top 10 (2023)
- OWASP ASVS v5.0 - V13: API and Web Service
- OWASP Testing Guide: WSTG-APIT
OWASP References
- OWASP API Security Top 10 (2023) — owasp.org/API-Security
- OWASP ASVS v5.0 — V13: API and Web Service Verification
- OWASP Testing Guide — WSTG-APIT: API Testing
- OWASP Cheat Sheet Series:
- REST Security
- GraphQL Security
- JWT Security
- OAuth 2.0
- SAML Security
- OWASP ZAP — API scanning capabilities