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.4 KiB
5.4 KiB
Session Management
Rule
Generate unpredictable session IDs. Bind sessions to users. Expire aggressively. Regenerate on privilege change.
Source: OWASP Session Management Cheat Sheet
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
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
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
# 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
# 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)