API Security
REST API Security Fundamentals
Section titled “REST API Security Fundamentals”REST APIs are stateless by design: each request must contain all information needed for Authentication and authorization. The server does not maintain session state between requests.
Stateless Authentication Per Request
Section titled “Stateless Authentication Per Request”Every request must include:1. Authentication credential (Bearer token, API key, mTLS certificate)2. Required headers (Content-Type, Accept)3. Any correlation/tracing identifiers
The server validates credentials on every request.No server-side session is required (but tokens must be stateless or validated).Authentication Methods
Section titled “Authentication Methods”API Keys
Section titled “API Keys”# Simple API key in headerAPI_KEYS = { "key_abc123': {'name': "service-a'', "scopes': ['read:users']}, 'key_def456': {'name': "service-b'', "scopes': ['read:users', 'write:orders']},}
@app.before_requestdef validate_api_key(): api_key = request.headers.get('X-API-Key') if not api_key or api_key not in API_KEYS: return jsonify({"error": "Invalid API key"}), 401 g.api_key_info = API_KEYS[api_key]Rate limit by user identity (API key or token) when authenticated, and by IP address when Unauthenticated. Unauthenticated rate limits should be stricter to prevent abuse.
Input Validation
Section titled “Input Validation”JSON Schema Validation
Section titled “JSON Schema Validation”from jsonschema import validate, ValidationError
CREATE_ORDER_SCHEMA = { "type": "object", "required": ["customer_id", "items"], "properties": { "customer_id": {"type": "integer", "minimum": 1}, "items": { "type": "array", "minItems": 1, "maxItems": 100, "items": { "type": "object", "required": ["product_id", "quantity"], "properties": { "product_id": {"type": "integer", "minimum": 1}, "quantity": {"type": "integer", "minimum": 1, "maximum": 999} } } }, "notes": {"type": "string", "maxLength": 1000} }, "additionalProperties": False}
@app.route('/api/orders', methods=['POST'])@require_bearer_token(['write:orders'])def create_order(): data = request.get_json() try: validate(data, CREATE_ORDER_SCHEMA) except ValidationError as e: return jsonify({"error": f"Validation failed: {e.message}"}), 400 ...Type Checking and Length Limits
Section titled “Type Checking and Length Limits”def validate_pagination(request): """Validate and sanitize pagination parameters.""" try: limit = int(request.args.get('limit', 20)) offset = int(request.args.get('offset', 0)) except (ValueError, TypeError): return None, None, "limit and offset must be integers"
limit = max(1, min(limit, 100)) # Clamp to [1, 100] offset = max(0, min(offset, 10000)) # Cap at 10000
return limit, offset, NoneCORS Configuration
Section titled “CORS Configuration”from flask_cors import CORS
# SAFE: explicit allowlistALLOWED_ORIGINS = [ 'https://app.example.com', 'https://admin.example.com']
CORS(app, origins=ALLOWED_ORIGINS, methods=['GET', 'POST', 'PUT', 'DELETE'], allow_headers=['Authorization', 'Content-Type'], supports_credentials=True)
# VULNERABLE: wildcard with credentials# CORS(app, origins='*', supports_credentials=True) # BLOCKED by browsersAPI Versioning
Section titled “API Versioning”| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL path | /api/v1/orders | Simple, visible | URL changes |
| Header | Accept: application/vnd.api.v1+json | Clean URLs | Hidden, harder to test |
| Query parameter | /api/orders?version=1 | Easy to add | Cache-busting issues |
# URL path versioning (recommended for most APIs)@app.route('/api/v1/orders')def list_orders_v1(): return jsonify(legacy_format(orders))
@app.route('/api/v2/orders')def list_orders_v2(): return jsonify(modern_format(orders))Response Filtering
Section titled “Response Filtering”# Sparse fieldsets (JSON:API pattern)@app.route('/api/v1/users')def list_users(): fields = request.args.get('fields', '').split(',') users = get_all_users()
if fields: filtered = [{k: u[k] for k in fields if k in u} for u in users] return jsonify(filtered) return jsonify(users)Pagination
Section titled “Pagination”Cursor vs Offset
Section titled “Cursor vs Offset”# Offset pagination (simple, but slow at high offsets)@app.route('/api/orders')def list_orders(): limit = min(int(request.args.get('limit', 20)), 100) offset = max(int(request.args.get('offset', 0)), 0) orders = db.query("SELECT * FROM orders ORDER BY id LIMIT %s OFFSET %s", (limit, offset)) return jsonify({"data": orders, "offset": offset, "limit": limit})
# Cursor pagination (efficient at any position)@app.route('/api/orders')def list_orders(): limit = min(int(request.args.get('limit', 20)), 100) cursor = request.args.get('cursor')
if cursor: orders = db.query( "SELECT * FROM orders WHERE id > %s ORDER BY id LIMIT %s", (cursor, limit) ) else: orders = db.query( "SELECT * FROM orders ORDER BY id LIMIT %s", (limit,) )
next_cursor = orders[-1]['id'] if orders else None return jsonify({ "data": orders, "next_cursor": next_cursor })| Method | Performance at page 10000 | Consistency on inserts/deletes | URL bookmarkable |
|---|---|---|---|
| Offset | Slow (scans 10000+ rows) | Unstable (rows shift) | Yes |
| Cursor | Fast (index lookup) | Stable (deterministic) | No (cursor changes) |
Idempotency Keys
Section titled “Idempotency Keys”from hashlib import sha256
@app.route('/api/orders', methods=['POST'])@require_bearer_token(['write:orders'])def create_order(): idempotency_key = request.headers.get('Idempotency-Key') if not idempotency_key: return jsonify({"error": "Idempotency-Key header required"}), 400
# Check if this key was already processed existing = redis.get(f"idempotency:{idempotency_key}") if existing: return jsonify(json.loads(existing)), 200 # Return original response
# Process the order order = create_order_from_request(request) response = jsonify(order)
# Store the response for future retries (TTL = 24h) redis.setex( f"idempotency:{idempotency_key}", 86400, json.dumps(order) )
return response, 201Webhook Security
Section titled “Webhook Security”import hmacimport hashlib
def verify_webhook_signature(request, secret): signature = request.headers.get('X-Webhook-Signature') if not signature: return False
payload = request.get_data() expected = hmac.new( secret.encode(), payload, hashlib.sha256 ).hexdigest()
return hmac.compare_digest(signature, expected)
@app.route('/webhooks/payment', methods=['POST'])def payment_webhook(): if not verify_webhook_signature(request, WEBHOOK_SECRET): return jsonify({"error": "Invalid signature"}), 401
event = request.get_json() process_payment_event(event) return jsonify({"status": "ok"}), 200API Gateway Patterns
Section titled “API Gateway Patterns”API Gateway responsibilities:1. Authentication (validate tokens, API keys)2. Rate limiting (per-client, per-route)3. Request transformation (headers, body)4. Response transformation (filtering, formatting)5. Load balancing (round-robin, least connections)6. Circuit breaking (fail fast on downstream failures)7. Request logging and monitoring8. TLS terminationGateway Implementation Options
Section titled “Gateway Implementation Options”| Option | Complexity | Customization | Use Case |
|---|---|---|---|
| Kong | Medium | High (Lua plugins) | Large-scale, extensible |
| AWS API Gateway | Low | Medium | AWS ecosystem |
| Envoy | High | Very High | Service mesh, gRPC |
| Nginx | Medium | High | General purpose, lightweight |
| Traefik | Low | Medium | Container environments |
GraphQL Security
Section titled “GraphQL Security”Query Depth Limiting
Section titled “Query Depth Limiting”# Limit maximum query depth to prevent complex/nested queriesMAX_QUERY_DEPTH = 5
def validate_query_depth(query_ast, current_depth=0): if current_depth > MAX_QUERY_DEPTH: raise GraphQLDepthError(f"Query depth exceeds maximum of {MAX_QUERY_DEPTH}")
for field in query_ast.selection_set.selections: if hasattr(field, 'selection_set') and field.selection_set: validate_query_depth(field, current_depth + 1)Complexity Analysis
Section titled “Complexity Analysis”# Assign cost to each field and limit total query complexityFIELD_COSTS = { 'orders': 10, 'items': 5, 'customer': 1,}
MAX_COMPLEXITY = 500
def calculate_complexity(query_ast): total = 0 for field in query_ast.selection_set.selections: field_name = field.name.value cost = FIELD_COSTS.get(field_name, 1) if hasattr(field, 'selection_set') and field.selection_set: cost *= calculate_complexity(field) total += cost return totalIntrospection Control
Section titled “Introspection Control”// Disable introspection in productionconst server = new ApolloServer({ typeDefs, resolvers, introspection: process.env.NODE_ENV !== 'production',});OpenAPI Security
Section titled “OpenAPI Security”# openapi.yaml security schemescomponents: securitySchemes: BearerAuth: type: http scheme: bearer bearerFormat: JWT
ApiKeyAuth: type: apiKey in: header name: X-API-Key
OAuth2: type: oauth2 flows: authorizationCode: authorizationUrl: https://auth.example.com/authorize tokenUrl: https://auth.example.com/token scopes: read:orders: Read orders write:orders: Create/update orders
security: - BearerAuth: [] - ApiKeyAuth: []
paths: /orders: get: security: - BearerAuth: [read:orders] post: security: - OAuth2: [write:orders]Common Pitfalls
Section titled “Common Pitfalls”Not Validating Input on the Server
Section titled “Not Validating Input on the Server”Client-side validation improves UX but provides zero security. An attacker can send any payload Directly to your API. Always validate on the server, regardless of what the client does.
CORS Misconfiguration
Section titled “CORS Misconfiguration”Reflecting the Origin header without validation allows any origin to make authenticated requests. Always use an explicit allowlist.
Exposing Internal IDs
Section titled “Exposing Internal IDs”Using sequential integer IDs (1, 2, 3…) allows attackers to enumerate resources. Use UUIDs or Hashids for public identifiers.
Not Rate Limiting Authentication Endpoints
Section titled “Not Rate Limiting Authentication Endpoints”Without rate limiting, attackers can brute-force credentials, enumerate usernames, and perform Credential stuffing attacks. Rate limit all authentication endpoints.
Returning Stack Traces in API Responses
Section titled “Returning Stack Traces in API Responses”Stack traces reveal implementation details (framework, library versions, file paths) that help Attackers craft targeted exploits. Return generic error messages in production; log details Server-side.
Summary
Section titled “Summary”This topic covers the essential concepts and techniques related to api security, including key principles and practical applications.
Key concepts include:
- core concepts and definitions
- key principles and frameworks
- practical applications
- common techniques and methods
- evaluation and critical analysis
A thorough understanding of these concepts, combined with regular practice and review, is essential for mastery of this topic.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.