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,185 @@
|
||||
# Session Management
|
||||
|
||||
## Rule
|
||||
|
||||
Generate unpredictable session IDs. Bind sessions to users. Expire aggressively. Regenerate on privilege change.
|
||||
|
||||
**Source:** [OWASP Session Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html)
|
||||
|
||||
## Session Attacks
|
||||
|
||||
| Attack | Description | Defense |
|
||||
|--------|-------------|---------|
|
||||
| Session fixation | Attacker sets victim's session ID | Regenerate on login |
|
||||
| Session hijacking | Steal session via XSS/network | httpOnly, Secure flags |
|
||||
| Session prediction | Guess valid session IDs | Cryptographic randomness |
|
||||
| Session replay | Reuse captured session | Short expiration, binding |
|
||||
|
||||
## Correct Pattern
|
||||
|
||||
```python
|
||||
import secrets
|
||||
from datetime import datetime, timedelta
|
||||
from flask import session, request
|
||||
|
||||
# Generate cryptographically secure session ID
|
||||
def generate_session_id() -> str:
|
||||
return secrets.token_urlsafe(32) # 256 bits of entropy
|
||||
|
||||
# Session configuration
|
||||
SESSION_CONFIG = {
|
||||
"cookie_name": "__Host-session", # __Host- prefix enforces Secure + no Domain
|
||||
"httponly": True, # Not accessible to JavaScript
|
||||
"secure": True, # HTTPS only
|
||||
"samesite": "Lax", # CSRF protection
|
||||
"max_age": 3600, # 1 hour max
|
||||
}
|
||||
|
||||
# Regenerate session on privilege change
|
||||
def login(user: User, password: str) -> bool:
|
||||
if not verify_password(user, password):
|
||||
return False
|
||||
|
||||
# CRITICAL: regenerate session ID to prevent fixation
|
||||
session.regenerate()
|
||||
|
||||
session["user_id"] = user.id
|
||||
session["login_time"] = datetime.utcnow().isoformat()
|
||||
session["ip"] = request.remote_addr
|
||||
session["user_agent"] = request.user_agent.string
|
||||
|
||||
return True
|
||||
|
||||
def logout():
|
||||
# Invalidate server-side, not just client cookie
|
||||
session_id = session.get("_id")
|
||||
if session_id:
|
||||
invalidate_session_server_side(session_id)
|
||||
session.clear()
|
||||
|
||||
# Validate session binding
|
||||
def validate_session() -> bool:
|
||||
if "user_id" not in session:
|
||||
return False
|
||||
|
||||
# Check session age
|
||||
login_time = datetime.fromisoformat(session.get("login_time", ""))
|
||||
if datetime.utcnow() - login_time > timedelta(hours=8):
|
||||
logout()
|
||||
return False
|
||||
|
||||
# Optional: bind to IP (careful with mobile/proxies)
|
||||
# if session.get("ip") != request.remote_addr:
|
||||
# logout()
|
||||
# return False
|
||||
|
||||
return True
|
||||
```
|
||||
|
||||
## Incorrect Pattern
|
||||
|
||||
```python
|
||||
import random
|
||||
import hashlib
|
||||
|
||||
# Wrong: predictable session ID
|
||||
def bad_session_id():
|
||||
return str(random.randint(1000000, 9999999))
|
||||
|
||||
# Wrong: sequential session ID
|
||||
COUNTER = 0
|
||||
def bad_session_id_2():
|
||||
global COUNTER
|
||||
COUNTER += 1
|
||||
return str(COUNTER)
|
||||
|
||||
# Wrong: user-derived session ID
|
||||
def bad_session_id_3(user_id):
|
||||
return hashlib.md5(str(user_id).encode()).hexdigest()
|
||||
|
||||
# Wrong: no regeneration on login (session fixation)
|
||||
def bad_login(user, password):
|
||||
if verify_password(user, password):
|
||||
session["user_id"] = user.id # Same session ID!
|
||||
return True
|
||||
return False
|
||||
|
||||
# Wrong: client-side only logout
|
||||
def bad_logout():
|
||||
return redirect("/", headers={"Set-Cookie": "session=; Max-Age=0"})
|
||||
# Session still valid server-side!
|
||||
|
||||
# Wrong: missing cookie security flags
|
||||
app.config["SESSION_COOKIE_HTTPONLY"] = False # XSS can steal
|
||||
app.config["SESSION_COOKIE_SECURE"] = False # Sent over HTTP
|
||||
```
|
||||
|
||||
## Session Fixation Attack
|
||||
|
||||
```python
|
||||
# Attack scenario:
|
||||
# 1. Attacker visits site, gets session ID "abc123"
|
||||
# 2. Attacker sends victim link: https://site.com/?sessionid=abc123
|
||||
# 3. Victim clicks, their browser now uses "abc123"
|
||||
# 4. Victim logs in (session ID unchanged!)
|
||||
# 5. Attacker uses "abc123" - now authenticated as victim
|
||||
|
||||
# Defense: ALWAYS regenerate on login
|
||||
@app.route("/login", methods=["POST"])
|
||||
def login():
|
||||
if authenticate(request.form):
|
||||
session.regenerate() # New session ID
|
||||
session["authenticated"] = True
|
||||
return redirect("/")
|
||||
```
|
||||
|
||||
## Concurrent Session Control
|
||||
|
||||
```python
|
||||
# Limit active sessions per user
|
||||
MAX_SESSIONS_PER_USER = 3
|
||||
|
||||
def create_session(user_id: str) -> str:
|
||||
# Get existing sessions
|
||||
existing = Session.query.filter_by(user_id=user_id).order_by(
|
||||
Session.created_at.asc()
|
||||
).all()
|
||||
|
||||
# Remove oldest if at limit
|
||||
if len(existing) >= MAX_SESSIONS_PER_USER:
|
||||
oldest = existing[0]
|
||||
oldest.delete()
|
||||
# Optionally notify user: "Logged out of oldest session"
|
||||
|
||||
# Create new session
|
||||
session_id = generate_session_id()
|
||||
Session.create(
|
||||
id=session_id,
|
||||
user_id=user_id,
|
||||
created_at=datetime.utcnow(),
|
||||
ip=request.remote_addr
|
||||
)
|
||||
return session_id
|
||||
|
||||
# Allow user to view/revoke sessions
|
||||
@app.route("/settings/sessions")
|
||||
def list_sessions():
|
||||
sessions = Session.query.filter_by(user_id=current_user.id).all()
|
||||
return render_template("sessions.html", sessions=sessions)
|
||||
|
||||
@app.route("/settings/sessions/<session_id>/revoke", methods=["POST"])
|
||||
def revoke_session(session_id):
|
||||
session = Session.query.get(session_id)
|
||||
if session and session.user_id == current_user.id:
|
||||
session.delete()
|
||||
return redirect("/settings/sessions")
|
||||
```
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- Mobile apps: use short-lived access tokens, not sessions
|
||||
- "Remember me": separate long-lived token, not extended session
|
||||
- Password change should invalidate all other sessions
|
||||
- Admin impersonation needs audit trail
|
||||
- Idle timeout vs absolute timeout (both needed)
|
||||
- Session data size limits (don't store large objects)
|
||||
Reference in New Issue
Block a user