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.
186 lines
5.4 KiB
Markdown
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)
|