Session Fixation & Management Code Review Guide
Table of Contents
Introduction
Session management is the backbone of web application authentication. Once a user logs in, the session is the only thing that keeps them authenticated across requests. Flaws in how sessions are created, maintained, and destroyed can give attackers full access to user accounts — without ever needing a password.
Session fixation is a particularly insidious attack because it doesn't require stealing a session — the attacker plants a session ID that the victim later authenticates. Combined with weak cookie configuration, missing expiration, and improper invalidation, session management vulnerabilities consistently appear in the OWASP Top 10.
Session Fixation vs Session Hijacking
Session fixation and session hijacking are both session attacks, but they work in opposite directions. In fixation, the attacker sets the session ID before the victim logs in. In hijacking, the attacker steals the session ID after the victim logs in. Both lead to account takeover, but they require different defenses.
Session Attack Vectors Comparison
- • Attacker sets the session ID before login
- • No need to steal anything
- • Works without XSS
- • Prevented by session regeneration
- • Attacker steals an active session ID
- • Requires XSS, sniffing, or log access
- • Session already authenticated
- • Prevented by secure cookie flags
- • Attacker reuses an expired or logged-out session
- • Exploits weak invalidation
- • Works if server doesn't destroy sessions
- • Prevented by server-side invalidation
What makes session fixation different from session hijacking?
Session Fixation Attacks
Session fixation occurs when an application accepts session identifiers from external sources — URL parameters, form fields, or injected cookies — and does not regenerate the session ID after authentication. The attacker obtains a valid session ID, tricks the victim into using it, and then shares the authenticated session.
Session Fixation Attack Flow
Key insight: The vulnerability exists because the application does not regenerate the session ID after authentication. The pre-auth and post-auth sessions share the same identifier.
Vulnerable: Session ID in URL Parameter
1// VULNERABLE: Accepting session ID from URL
2app.get('/login', (req, res) => {
3 // Attacker sends: https://example.com/login?sessionId=ATTACKER_KNOWN_ID
4 if (req.query.sessionId) {
5 req.session.id = req.query.sessionId; // Using attacker-supplied ID!
6 }
7 res.render('login');
8});
9
10app.post('/login', async (req, res) => {
11 const { username, password } = req.body;
12 if (await authenticate(username, password)) {
13 // Session ID is NOT regenerated!
14 // The attacker-supplied ID is now authenticated
15 req.session.user = username;
16 req.session.authenticated = true;
17 res.redirect('/dashboard');
18 }
19});Vulnerable: No Session Regeneration on Login
1# VULNERABLE: Django view without session regeneration
2from django.contrib.auth import authenticate, login
3
4def login_view(request):
5 if request.method == 'POST':
6 username = request.POST['username']
7 password = request.POST['password']
8 user = authenticate(request, username=username, password=password)
9 if user is not None:
10 # login() sets request.session['_auth_user_id']
11 # but if cycle_key() is not called, the session ID stays the same
12 login(request, user)
13 # Missing: request.session.cycle_key()
14 return redirect('/dashboard')
15 return render(request, 'login.html')
16
17# VULNERABLE: PHP session fixation
18# <?php
19# // Attacker visits: https://example.com/login.php?PHPSESSID=KNOWN_VALUE
20# session_start(); // Accepts the attacker's session ID
21# if (authenticate($username, $password)) {
22# $_SESSION['authenticated'] = true;
23# // session_regenerate_id(true) is NOT called!
24# }Vulnerable: Cookie Injection via Subdomain
1// VULNERABLE: Cookie scoped too broadly
2app.use(session({
3 secret: process.env.SESSION_SECRET,
4 cookie: {
5 domain: '.example.com', // Any subdomain can set this cookie!
6 // attacker.example.com can inject: Set-Cookie: sessionId=KNOWN_VALUE; Domain=.example.com
7 }
8}));
9
10// If an attacker controls any subdomain (e.g. via subdomain takeover),
11// they can inject a session cookie for the parent domain.
12// When the victim visits the main app, they use the attacker's session ID.Hidden Fixation Vector: Meta Tags and JavaScript
Session fixation isn't limited to URL parameters. Attackers can set cookies via <meta http-equiv="Set-Cookie"> tags in injected HTML, or via document.cookie if they find an XSS vulnerability. Even cross-subdomain cookie injection is possible when the cookie's Domain attribute is set too broadly.
Which of the following is NOT a session fixation attack vector?
Session ID Generation
A session ID must be unpredictable enough that an attacker cannot guess or brute-force it. Weak session IDs — based on timestamps, sequential counters, or weak random number generators — make session prediction and fixation attacks trivial.
Vulnerable Session ID Generation
1// VULNERABLE: Timestamp-based session ID
2function generateSessionId() {
3 return Date.now().toString(36); // Predictable: based on current time
4}
5
6// VULNERABLE: Sequential session IDs
7let counter = 1000;
8function generateSessionId() {
9 return 'sess_' + (++counter); // Trivially guessable
10}
11
12// VULNERABLE: Math.random() is not cryptographically secure
13function generateSessionId() {
14 return Math.random().toString(36).substring(2);
15 // Internal state can be recovered from outputs
16}
17
18// VULNERABLE: Weak entropy
19function generateSessionId() {
20 const pid = process.pid;
21 const time = Date.now();
22 return require('crypto').createHash('md5')
23 .update(pid + ':' + time).digest('hex');
24 // Only two inputs, both guessable
25}Secure Session ID Generation
1const crypto = require('crypto');
2
3// SECURE: Cryptographically random session ID
4function generateSessionId() {
5 return crypto.randomBytes(32).toString('hex');
6 // 256 bits of entropy — infeasible to brute-force
7}
8
9// SECURE: Using uuid v4 (backed by crypto.randomUUID)
10function generateSessionId() {
11 return crypto.randomUUID();
12}
13
14// SECURE: Custom format with sufficient entropy
15function generateSessionId() {
16 const bytes = crypto.randomBytes(32);
17 return 'sess_' + bytes.toString('base64url');
18 // base64url encoding: URL-safe, no padding
19}
20
21// The session ID should have at least 128 bits of entropy.
22// OWASP recommends a minimum of 128 bits (16 bytes).
23// Using 256 bits (32 bytes) provides a strong safety margin.Session ID Entropy Requirements
| Method | Entropy | Security Level |
|---|---|---|
| Date.now() | ~40 bits | ❌ Trivially predictable |
| Math.random() | ~52 bits | ❌ Not cryptographic |
| UUID v1 (time-based) | ~60 bits unique | ⚠️ Partially predictable |
| crypto.randomBytes(16) | 128 bits | ✅ OWASP minimum |
| crypto.randomBytes(32) | 256 bits | ✅ Recommended |
Why is Math.random() unsuitable for session ID generation?
Session Lifecycle
Proper session management requires attention at every stage: creation, authentication binding, validation, privilege changes, and destruction. Many vulnerabilities arise from skipping steps in this lifecycle — particularly session regeneration and server-side invalidation.
Secure Session Lifecycle
Vulnerable Session Lifecycle
1// VULNERABLE: No session regeneration on login
2app.post('/login', async (req, res) => {
3 if (await authenticate(req.body.username, req.body.password)) {
4 req.session.user = req.body.username;
5 req.session.role = 'user';
6 // Session ID stays the same! Fixation possible.
7 res.redirect('/dashboard');
8 }
9});
10
11// VULNERABLE: No regeneration on privilege escalation
12app.post('/admin/elevate', (req, res) => {
13 if (req.session.role === 'user' && verifyAdminCode(req.body.code)) {
14 req.session.role = 'admin';
15 // Same session ID now has admin privileges!
16 // If it was fixated or leaked as user-level, attacker gets admin
17 res.redirect('/admin');
18 }
19});
20
21// VULNERABLE: Client-side only logout
22app.post('/logout', (req, res) => {
23 res.clearCookie('sessionId');
24 // Session data still exists on the server!
25 // If attacker captured the session ID, it's still valid
26 res.redirect('/login');
27});
28
29// VULNERABLE: No absolute timeout
30app.use(session({
31 cookie: { maxAge: null }, // Never expires!
32 rolling: true, // Activity keeps extending it forever
33}));Secure Session Lifecycle
1// SECURE: Regenerate session ID on authentication
2app.post('/login', async (req, res) => {
3 if (await authenticate(req.body.username, req.body.password)) {
4 // Regenerate session — new ID, old data preserved
5 req.session.regenerate((err) => {
6 if (err) return res.status(500).send('Session error');
7 req.session.user = req.body.username;
8 req.session.role = 'user';
9 req.session.loginTime = Date.now();
10 req.session.lastActivity = Date.now();
11 res.redirect('/dashboard');
12 });
13 }
14});
15
16// SECURE: Regenerate on privilege change
17app.post('/admin/elevate', (req, res) => {
18 if (verifyAdminCode(req.body.code)) {
19 req.session.regenerate((err) => {
20 if (err) return res.status(500).send('Session error');
21 req.session.role = 'admin';
22 req.session.elevatedAt = Date.now();
23 res.redirect('/admin');
24 });
25 }
26});
27
28// SECURE: Server-side session destruction on logout
29app.post('/logout', (req, res) => {
30 req.session.destroy((err) => {
31 if (err) return res.status(500).send('Logout error');
32 res.clearCookie('__Host-sid');
33 res.redirect('/login');
34 });
35});
36
37// SECURE: Idle timeout AND absolute timeout
38function sessionTimeoutMiddleware(req, res, next) {
39 if (!req.session.loginTime) return next();
40
41 const now = Date.now();
42 const IDLE_TIMEOUT = 30 * 60 * 1000; // 30 minutes
43 const ABSOLUTE_TIMEOUT = 8 * 60 * 60 * 1000; // 8 hours
44
45 if (now - req.session.lastActivity > IDLE_TIMEOUT) {
46 return req.session.destroy(() => res.redirect('/login?reason=idle'));
47 }
48
49 if (now - req.session.loginTime > ABSOLUTE_TIMEOUT) {
50 return req.session.destroy(() => res.redirect('/login?reason=expired'));
51 }
52
53 req.session.lastActivity = now;
54 next();
55}Regenerate, Don't Just Delete
When regenerating a session, use your framework's built-in regeneration method (e.g., req.session.regenerate() in Express, request.session.cycle_key() in Django). Don't manually delete and recreate — race conditions can cause the old session to persist briefly, and you may lose session data needed during the transition.
When should you regenerate the session ID?
Prevention Techniques
Preventing session management vulnerabilities requires a defense-in-depth approach. No single measure is sufficient — combine session regeneration, secure cookies, server-side storage, timeouts, and monitoring to build a robust session management system.
- Regenerate session ID on auth changes: Always call session regeneration on login, logout, privilege escalation, and password changes.
- Use secure cookie attributes: Set
HttpOnly,Secure,SameSite=Lax(or Strict), and use the__Host-prefix. - Generate cryptographically random IDs: Use
crypto.randomBytes(32)or framework defaults that use CSPRNGs. Never accept session IDs from URL parameters. - Implement server-side session storage: Store session data in Redis, Memcached, or a database — never in the cookie itself (signed cookies still leak data).
- Enforce idle and absolute timeouts: Set an idle timeout (15-30 minutes) and an absolute timeout (4-8 hours). Don't let rolling sessions live forever.
- Destroy sessions server-side on logout: Clear the server-side session data AND the client cookie. Never rely on client-side deletion alone.
- Reject session IDs from URLs: Never accept
?sessionId=or?PHPSESSID=parameters. Disable URL-based session tracking in framework config. - Bind sessions to client fingerprint: Optionally bind sessions to User-Agent or IP range to detect session migration between clients.
Code Review Checklist Implementation
1// Session management security checklist as middleware
2const sessionSecurityMiddleware = {
3 // 1. Reject session IDs from URL parameters
4 rejectUrlSessions(req, res, next) {
5 if (req.query.sessionId || req.query.sid || req.query.PHPSESSID) {
6 return res.status(400).send('Session IDs in URLs are not accepted');
7 }
8 next();
9 },
10
11 // 2. Validate session integrity on each request
12 validateSession(req, res, next) {
13 if (!req.session || !req.session.user) return next();
14
15 // Check idle timeout
16 const idleLimit = 30 * 60 * 1000;
17 if (Date.now() - req.session.lastActivity > idleLimit) {
18 return req.session.destroy(() => res.redirect('/login'));
19 }
20
21 // Check absolute timeout
22 const absoluteLimit = 8 * 60 * 60 * 1000;
23 if (Date.now() - req.session.createdAt > absoluteLimit) {
24 return req.session.destroy(() => res.redirect('/login'));
25 }
26
27 // Update activity timestamp
28 req.session.lastActivity = Date.now();
29 next();
30 },
31
32 // 3. Concurrent session limiting
33 async limitConcurrentSessions(req, res, next) {
34 if (!req.session.user) return next();
35
36 const activeSessions = await sessionStore.findByUser(req.session.user);
37 const MAX_SESSIONS = 3;
38
39 if (activeSessions.length > MAX_SESSIONS) {
40 // Destroy oldest sessions
41 const toDestroy = activeSessions
42 .sort((a, b) => a.lastActivity - b.lastActivity)
43 .slice(0, activeSessions.length - MAX_SESSIONS);
44
45 for (const s of toDestroy) {
46 await sessionStore.destroy(s.id);
47 }
48 }
49 next();
50 }
51};Session Security Configuration Checklist
| Setting | Secure Value | Why It Matters |
|---|---|---|
| Cookie HttpOnly | true | Blocks XSS-based session theft |
| Cookie Secure | true | Prevents HTTP interception |
| Cookie SameSite | Lax or Strict | Blocks CSRF token reuse |
| Cookie Name Prefix | __Host- | Prevents subdomain injection |
| Session Storage | Server-side (Redis) | Client can't tamper with data |
| Idle Timeout | 15-30 minutes | Limits window for stolen sessions |
| Absolute Timeout | 4-8 hours | Forces periodic re-authentication |
| Session Regeneration | On every auth change | Defeats fixation attacks |
| URL Session IDs | Disabled | Prevents fixation via URL |
| Concurrent Sessions | Limited (2-3) | Detects session sharing/theft |