Files
security-patterns/session-management.md
T
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

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)