Files
security-patterns/session-management.md
Rodin 17c535bc61 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.
2026-05-10 23:20:36 -07:00

186 lines
5.4 KiB
Markdown

# 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)