Comprehensive API security assessment against OWASP API Security Top 10 (2023) with real-world attack scenarios, automated testing integration, and detailed remediation guidance.
Use this play when:
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):
app.get(), router.post()path(), url()resources, get/post/put/delete@GetMapping, @RequestMapping@app.get(), decorator-based routesKey Questions:
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 |
Common JWT Vulnerabilities:
Attack: Change algorithm from RS256 to none or HS256
Header: {"alg":"none","typ":"JWT"}
Result: Token accepted without verification
# 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
Check: Does payload contain "exp" claim?
Risk: Tokens valid forever if stolen
// Insecure: localStorage (XSS accessible)
localStorage.setItem('jwt', token);
// More secure: httpOnly cookie
document.cookie = "jwt=" + token + "; HttpOnly; Secure; SameSite=Strict";
Testing Checklist:
Critical Checks:
Required: PKCE (Proof Key for Code Exchange)
Check: code_challenge and code_verifier parameters
Risk: Without PKCE, auth codes can be stolen and used
Required: Random state parameter to prevent CSRF
Check: Is state validated on callback?
Attack: Omit state → CSRF login attack
Check: Is redirect_uri strictly validated?
Attack: redirect_uri=https://attacker.com/callback
Risk: Authorization code/token sent to attacker
Check: Can user modify requested scopes?
Attack: Change scope=read to scope=admin
Risk: Privilege escalation via scope manipulation
Testing Checklist:
Common Issues:
# 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
Issue: Single key with full access
Best Practice: Scoped keys (read-only, write, admin)
Check: Can keys be rotated without downtime?
Risk: Compromised keys valid indefinitely
Risk: Attackers access other users’ data by manipulating object IDs
Real-World Impact:
Attack Scenarios:
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
If using predictable UUIDv1 (timestamp-based):
Generate nearby UUIDs to find other users
POST /api/orders/bulk
{
"order_ids": [1001, 1002, 1003, 1004] # Can access any IDs?
}
GET /api/orders?id=1001
# vs
GET /api/orders?id=1001&user_id=123 # Is user_id validated?
Testing Approach:
# 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
Risk: Weak authentication allows account takeover
Real-World Impact:
Attack Scenarios:
# Automated login attempts with breached credentials
# APIs often lack CAPTCHA or rate limiting
Header: {"alg":"none","typ":"JWT"}
Payload: {"user_id":1,"role":"admin"}
Signature: (empty)
Result: Server accepts unsigned token
POST /api/login
{"username":"admin","password":"123456"}
# Test common passwords
# Check minimum length requirements
Attacker sets session ID
Victim logs in with attacker's session ID
Attacker uses same session ID to access account
Testing Approach:
# 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
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),
}
Risk: Mass assignment and data over-exposure
Real-World Impact:
Attack Scenarios:
POST /api/users
{
"username": "newuser",
"email": "user@example.com",
"password": "password123",
"role": "admin", # Should not be user-settable
"is_verified": true # Should not be user-settable
}
GET /api/users/123
Response:
{
"id": 123,
"username": "john",
"email": "john@example.com",
"password_hash": "$2b$12$...", # Should not be exposed!
"ssn": "123-45-6789", # PII exposure
"internal_notes": "Difficult customer"
}
PUT /api/users/123
{
"salary": 200000 # Can regular users modify this?
}
Testing Approach:
# Send extra fields in POST/PUT requests
payload = {
"username": "test",
"email": "test@test.com",
"password": "password",
"role": "admin", # Try to escalate
"is_admin": True,
"credit_balance": 10000
}
# 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
Risk: DoS via resource exhaustion, financial drain
Real-World Impact:
Attack Scenarios:
# Distributed attack from multiple IPs
# Or using different API keys
# Or changing User-Agent headers
# Deeply nested GraphQL query
query {
users {
posts {
comments {
author {
posts {
comments {
author {
posts { ... } # Infinite recursion potential
}
}
}
}
}
}
}
}
POST /api/upload
Content-Length: 1000000000 # 1GB file
# Or JSON bomb:
{"a":{"a":{"a":{"a":...}}}} # Deeply nested JSON
GET /api/items?limit=1000000 # Request all records
GET /api/items?page=1&per_page=10000 # Large page size
Testing Approach:
# 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
# Test query complexity
# Test query depth
# Test expensive operations
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
Risk: Access to admin/sensitive functions without proper authorization
Real-World Impact:
Attack Scenarios:
GET /api/admin/users # Try common admin paths
GET /api/v1/admin/config
GET /api/internal/audit-logs
GET /api/debug/info
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?
POST /api/users
{
"username": "test",
"role": "admin" # Is this validated server-side?
}
Testing Approach:
# Use wordlists to find admin endpoints
# Common paths: /admin, /internal, /debug, /system
# 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
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)
Risk: Automated abuse of business logic
Real-World Impact:
Attack Scenarios:
POST /api/cart/add
{"product_id": 123, "quantity": 999999} # Buy all inventory
# Script that completes purchase faster than humans
# Bypassing any client-side rate limits
POST /api/order
{
"items": [...],
"total": 1.00 # Is total calculated server-side?
}
POST /api/reservations
{"resource_id": 123, "start": "2024-01-01", "end": "2025-12-31"}
# Reserve resource for entire year
Testing Approach:
# 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
Risk: Server makes requests to attacker-controlled or internal resources
Real-World Impact:
Attack Scenarios:
GET /api/fetch?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Retrieves AWS credentials
GET /api/fetch?url=http://localhost:8080/admin
GET /api/fetch?url=http://internal-api.company.local/secrets
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
Testing Approach:
# 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"
# 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]/"
# 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)
Risk: Insecure default configurations expose APIs to attacks
Real-World Impact:
Attack Scenarios:
Access-Control-Allow-Origin: * # Too permissive
Access-Control-Allow-Credentials: true # Dangerous combination
{
"error": "SQL syntax error near 'user_id = 123'",
"stack_trace": "...",
"query": "SELECT * FROM users WHERE user_id = 123"
}
# Missing headers:
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Content-Security-Policy: ...
Strict-Transport-Security: max-age=31536000
/api/debug/vars # Exposes Go runtime variables
/api/swagger-ui/ # API documentation with try-it feature
/api/actuator/health # Spring Boot endpoints
Testing Approach:
curl -I https://api.example.com/endpoint | grep -i "access-control\|x-"
# Trigger errors and check responses
curl "https://api.example.com/endpoint?param=' OR 1=1--"
curl "https://api.example.com/endpoint" -X INVALID_METHOD
# 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
)
Risk: Shadow APIs and undocumented endpoints create attack surface
Real-World Impact:
Attack Scenarios:
Compare OpenAPI spec with actual code
Look for endpoints not in documentation
GET /api/v1/users # Old version still active
GET /api/v2/users # New version
# v1 may have known vulnerabilities
Production API: api.company.com
Staging API: api-staging.company.com # Exposed?
Dev API: api-dev.company.com # Exposed with debug enabled?
Testing Approach:
# Extract all routes from code
grep -r "@app.route\|@GetMapping\|path(" --include="*.py" --include="*.java"
# Compare with OpenAPI spec
# Identify undocumented endpoints
# Test old API versions
curl https://api.example.com/v1/users
curl https://api.example.com/v2/users
# 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
Risk: Trusting third-party API responses leads to injection attacks
Real-World Impact:
Attack Scenarios:
<!-- Malicious response from third-party API -->
<?xml version="1.0"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data>&xxe;</data>
# Attacker controls serialized data
import pickle
malicious = b'...crafted pickle payload...'
data = pickle.loads(malicious) # RCE!
# 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
// Malicious JSON from external API
{
"username": "admin",
"bio": "<script>alert('XSS')</script>"
}
Testing Approach:
# If API accepts XML from third parties
# Send malicious XML to test for XXE
# Check for unsafe deserialization
# Look for pickle, ObjectInputStream, unserialize
# 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
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:
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");
});
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!")
# 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"
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;
}
}
# 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
## 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