This vulnerability occurs when an application fails to properly invalidate user sessions after a period of inactivity (idle timeout) or after a maximum total duration (absolute timeout). If sessions remain valid indefinitely or for excessively long periods (e.g., months or years), the risk of session hijacking increases significantly. An attacker who manages to steal a session identifier (e.g., via XSS, network sniffing if not Secure, or guessing) has a much larger window of opportunity to reuse it. Furthermore, users accessing the application from shared computers might leave their sessions active inadvertently if there’s no timeout. ⏳➡️🔓
Session expiration is typically managed through framework configuration settings that control cookie expiry and server-side session data lifetime.Key Remediation Principles:
Implement Idle Timeout: Automatically log out users after a period of inactivity (e.g., 15-30 minutes). This involves tracking the last activity time on the server-side session.
Implement Absolute Timeout: Automatically log out users after a fixed maximum duration, regardless of activity (e.g., 8-24 hours). This limits the maximum lifetime of any session identifier.
Secure Logout Functionality: Provide a clear logout button that properly invalidates the session on the server-side.
Cookie Expiry vs. Server Expiry: Ensure both the session cookie’s expiry (Expires/Max-Age attributes) and the server-side session data’s lifetime are configured appropriately. Server-side expiration is generally more critical.
Configuring SESSION_COOKIE_AGE and SESSION_SAVE_EVERY_REQUEST in Django settings.py. Flask requires configuring the session interface (e.g., PERMANENT_SESSION_LIFETIME for Flask-Session).
Vulnerable Scenario 1: Django Session Never Expires
# settings.py (Django)# DANGEROUS: Setting age to None or a very large number means the session# cookie might persist indefinitely based on browser settings, and server-side# data might not be cleaned up efficiently depending on session backend.SESSION_COOKIE_AGE = None # Or a huge value like 315360000 (10 years)# DANGEROUS: If False (default is False), idle timeout doesn't work effectively.# The session expiry is only updated when the session is modified.SESSION_SAVE_EVERY_REQUEST = False
Flask’s default client-side cookie session typically lasts only for the browser session unless session.permanent = True is set along with PERMANENT_SESSION_LIFETIME. If using server-side sessions (like Flask-Session), defaults might be too long or missing.
# app.py (Flask - built-in session)app.secret_key = '...' # Needed for session# No PERMANENT_SESSION_LIFETIME configured, session might expire only on browser close.@app.route('/login')def login(): # ... login logic ... # DANGEROUS: If PERMANENT_SESSION_LIFETIME is very long or default (31 days). session.permanent = True # Mark session to use lifetime # ...
Django: Set SESSION_COOKIE_AGE to a reasonable absolute timeout (e.g., 8 * 60 * 60 for 8 hours). Set SESSION_SAVE_EVERY_REQUEST = True to enable idle timeout behavior (session expiry is reset on each request). Configure the session cleanup mechanism (./manage.py clearsessions).
Flask: Set PERMANENT_SESSION_LIFETIME (a timedelta object or seconds) to a reasonable absolute timeout. Ensure session.permanent = True is set after login if using this lifetime. For idle timeout, custom logic tracking last activity time in the session is usually needed.
Idle Timeout: Leave the browser inactive for longer than the configured idle timeout period. Try performing an action (e.g., refresh page, click link). You should be logged out or prompted to log back in.
Absolute Timeout: Note the login time. Wait longer than the configured absolute timeout period (even if you were active). Try performing an action. You should be logged out.
Inspect the session cookie’s Expires/Max-Age attribute in browser developer tools (though server-side expiration is more important).
Configuring session timeout in application.properties (server.servlet.session.timeout) for Spring Boot, or session-timeout in web.xml for traditional Servlets.
Vulnerable Scenario 1: No Timeout Configured (Servlet Default)
In older servlet containers or if not explicitly configured, the default timeout might be too long (e.g., 30 minutes, but could be longer or server-dependent).
Vulnerable Scenario 2: Excessively Long Timeout (Spring Boot)
# application.properties# DANGEROUS: Setting timeout to 24 hours allows sessions to persist too long.# Should be much shorter for idle timeout.server.servlet.session.timeout=24h# Or using seconds: server.servlet.session.timeout=86400
Test idle and absolute timeouts as described for Python. Log in, wait longer than the configured server.servlet.session.timeout without activity, then try accessing a protected resource. Verify logout/redirect. Check server configuration files (application.properties, web.xml) for timeout settings.
// Startup.cs (ConfigureServices)services.AddSession(options =>{ // DANGEROUS: Very long idle timeout (e.g., 8 hours). // User stays logged in as long as they click something every 7 hours. options.IdleTimeout = TimeSpan.FromHours(8); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true;});
Vulnerable Scenario 2: Long Auth Cookie Lifetime without Sliding Expiration
// Startup.cs (ConfigureServices)services.ConfigureApplicationCookie(options =>{ // DANGEROUS: Cookie lasts for 30 days regardless of activity. options.ExpireTimeSpan = TimeSpan.FromDays(30); // DANGEROUS: SlidingExpiration is off (or default might be off). // Combined with long ExpireTimeSpan, this is very insecure. options.SlidingExpiration = false;});
Session: Set options.IdleTimeout to a short duration (e.g., 15-30 minutes).
Auth Cookie: Set options.ExpireTimeSpan to a moderate absolute lifetime (e.g., 8-12 hours). Enable options.SlidingExpiration = true so the cookie expiry is renewed on activity, but only up to the ExpireTimeSpan. This provides both idle and absolute timeout behavior for the authentication cookie.
Idle: Log in. Wait longer than IdleTimeout (session) or the effective idle period implied by SlidingExpiration (auth cookie). Try accessing resources. Verify logout/redirect. Note that SlidingExpiration renews the cookie, effectively acting like an idle timeout up to the ExpireTimeSpan.
Absolute: Log in. Wait longer than ExpireTimeSpan. Try accessing resources. Verify logout/redirect, even if active during the period.
Check Startup.cs configuration.
Controlled by session.gc_maxlifetime in php.ini (server-side idle timeout) and session.cookie_lifetime (client-side cookie expiry). Laravel uses lifetime and expire_on_close in config/session.php.
Vulnerable Scenario 1: Long gc_maxlifetime (Plain PHP)
; php.ini; DANGEROUS: Session data persists on server for 1 day of inactivity.session.gc_maxlifetime = 86400 ; (seconds = 24 hours); DANGEROUS: Cookie persists even after browser close if lifetime > 0.session.cookie_lifetime = 0 ; 0 means until browser close, but long gc_maxlifetime is still risky.; If cookie_lifetime was set to 86400*30 (30 days), it's even worse.
Plain PHP: Set session.gc_maxlifetime to a reasonable idle timeout in seconds (e.g., 1800 for 30 minutes). Set session.cookie_lifetime = 0 to make it a session cookie (expires on browser close) which relies on gc_maxlifetime for idle timeout.
Laravel: Set 'lifetime' (in minutes) to a reasonable idle timeout (e.g., 30). Set 'expire_on_close' => false (default) to respect the idle timeout. Consider implementing manual absolute timeout logic if needed.
Test idle timeout by logging in, waiting longer than session.gc_maxlifetime (PHP) or lifetime (Laravel), and trying an action. Test session.cookie_lifetime / expire_on_close behavior by closing and reopening the browser. Check php.ini and config/session.php settings.
// app.jsapp.use(session({ secret: 'my-secret', cookie: { // DANGEROUS: maxAge is omitted. Cookie might be session-only (browser close) // or have a long default depending on session store. Server-side store // might also lack cleanup. Effectively no reliable timeout. httpOnly: true, secure: isProd }}));
Test idle timeout (if rolling: true) by waiting longer than maxAge without activity. Test absolute timeout (if rolling: false) by waiting longer than maxAge even with activity. Inspect the cookie’s Expires/Max-Age attribute. Check express-session configuration options.
# config/initializers/session_store.rb# DANGEROUS: No :expire_after option set. Cookie might be session-only,# or rely on server defaults. No explicit timeout configured.Rails.application.config.session_store :cookie_store, key: '_my_app_session', httponly: true, secure: Rails.env.production?
The :timeoutable module is not included in the Devise model, disabling idle timeout.
# app/models/user.rbclass User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :omniauthable # DANGEROUS: :timeoutable module is missing. User stays logged in indefinitely unless cookie expires. devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable # Missing :timeoutableend# config/initializers/devise.rb# config.timeout_in = 30.minutes # This setting has no effect if model doesn't use :timeoutable
Rails Sessions: Set the :expire_after option in session_store.rb to configure an absolute session lifetime tied to the cookie. Implement manual idle timeout tracking in the session if needed.
Devise: Include the :timeoutable module in your User model. Configure config.timeout_in in config/initializers/devise.rb to set the idle timeout duration (e.g., 30.minutes).
# app/models/user.rb (Secure - Devise Idle Timeout)class User < ApplicationRecord devise :database_authenticatable, :registerable, :recoverable, :rememberable, :validatable, # SECURE: Include timeoutable module. :timeoutableend# config/initializers/devise.rb (Secure - Devise Idle Timeout)Devise.setup do |config| # ==> Configuration for :timeoutable # The time you want the user session to be expired after inactivity. # SECURE: Set idle timeout duration (e.g., 30 minutes). config.timeout_in = 30.minutes # ... other devise settings ...end
Test idle timeout (if using Devise :timeoutable or manual tracking) by waiting longer than config.timeout_in without activity. Test absolute timeout (if using expire_after) by waiting longer than the specified duration. Check session_store.rb and devise.rb configurations. Inspect cookie Expires/Max-Age.