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