Session Expiration

Long-lived SessionsMissing Idle Timeout

Session Expiration at a glance

What it is: Sessions remain valid for too long or do not expire on inactivity, allowing stolen tokens to be reused.
Why it happens: Extends the window for session hijacking and unauthorized access
How to fix: Set short idle timeouts with sliding expiration and a hard absolute cap; Rotate and revoke on logout, password change, and MFA enrollment changes; Bind sessions to device or context where possible, and set secure cookie flags

Overview

Session expiration controls limit how long authenticated state persists, especially after inactivity. When applications omit idle timeouts or set very long lifetimes, stolen cookies or tokens remain usable for weeks. Attackers can replay old cookies to access accounts and perform sensitive operations without reauthentication.

sequenceDiagram participant Browser participant App as App Server Browser->>App: GET /account (Cookie: connect.sid=<old>) App-->>Browser: 200 OK, user account data note over App: No idle timeout or absolute expiration on session
A potential flow for a Session Expiration exploit

Where it occurs

Common causes are frameworks left at permissive defaults, custom remember-me implementations without rotation, missing server-side TTLs, and ignoring recent-auth requirements for high-value actions.

Impact

A single cookie theft leads to long-term account access, payment method changes, data exfiltration, and difficulty in incident containment because old tokens continue to work.

Prevention

Use short idle timeouts with sliding refresh, plus an absolute lifetime cap. Rotate remember-me tokens and invalidate all sessions on password change and logout. Store server-side session indices and enforce TTLs. Add device binding where feasible and use secure cookie attributes (HttpOnly, Secure, SameSite). Require recent authentication for sensitive actions.

Examples

Switch tabs to view language/framework variants.

Session cookie set without expiry or idle timeout

Sessions persist indefinitely on the server and client, increasing hijack window if tokens leak.

Vulnerable
JavaScript • Express — Bad
const session = require('express-session');
app.use(session({
  secret: 'devsecret',
  resave: false,
  saveUninitialized: false // BUG: no expiration configured
}));
  • Line 5: No maxAge or server-side TTL, sessions live too long

Without an idle timeout, stolen sessions remain valid for a long period.

Secure
JavaScript • Express — Good
const session = require('express-session');
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  cookie: { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 30*60*1000 },
  rolling: true // refresh expiry on activity
}));
  • Line 7: Idle timeout via cookie maxAge and rolling refresh

Short idle timeouts and secure cookie flags reduce the window of abuse.

Engineer Checklist

  • Set idle timeout to 15–30 minutes with sliding expiration

  • Set an absolute session lifetime cap, for example 8–24 hours for web sessions

  • Rotate and revoke remember-me tokens and sessions on logout and password change

  • Use secure cookie flags and bind sessions to device where possible

  • Require recent authentication for payment, MFA, and email changes

End-to-End Example

A legacy portal keeps sessions valid for 30 days and does not refresh or revoke them on sensitive changes. An attacker who stole a cookie during a cafe Wi-Fi session can still access the account a month later.

Vulnerable
JAVASCRIPT
// Node.js/Express - Vulnerable session expiration

const session = require('express-session');

app.use(session({
  secret: 'my-secret',
  resave: false,
  saveUninitialized: false,
  cookie: {
    // VULNERABLE: No maxAge set - session cookie persists until browser close
    // And browser may keep session alive across restarts
    secure: false,  // Also vulnerable: not requiring HTTPS
    httpOnly: true
  }
  // VULNERABLE: No session store TTL - sessions never expire server-side!
}));

app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findOne({ email });
  
  if (user && await bcrypt.compare(password, user.passwordHash)) {
    req.session.userId = user._id;
    // VULNERABLE: No tracking of last activity time
    // No absolute session creation time either
    res.json({ message: 'Login successful' });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

// VULNERABLE: Remember-me with permanent, non-rotating token
app.post('/remember-me', authenticateSession, (req, res) => {
  const user = req.user;
  
  // Generate remember token (once, never rotated)
  const rememberToken = crypto.randomBytes(32).toString('hex');
  
  // VULNERABLE: Store permanently, no expiration
  await User.findByIdAndUpdate(user._id, { rememberToken });
  
  // VULNERABLE: Set permanent cookie (no maxAge)
  res.cookie('remember_me', rememberToken, {
    httpOnly: true,
    // No maxAge - cookie lives forever!
  });
  
  res.json({ message: 'Remember me enabled' });
});

// VULNERABLE: Password change doesn't invalidate sessions
app.post('/change-password', authenticateSession, async (req, res) => {
  const { currentPassword, newPassword } = req.body;
  
  const user = await User.findById(req.session.userId);
  const valid = await bcrypt.compare(currentPassword, user.passwordHash);
  
  if (!valid) {
    return res.status(401).json({ error: 'Current password incorrect' });
  }
  
  const newHash = await bcrypt.hash(newPassword, 12);
  await User.findByIdAndUpdate(user._id, { passwordHash: newHash });
  
  // VULNERABLE: Old sessions remain valid!
  // Attacker who stole session can still access after password change
  res.json({ message: 'Password changed' });
});
Secure
JAVASCRIPT
// Node.js/Express - Secure session expiration

const session = require('express-session');
const RedisStore = require('connect-redis').default;
const redis = require('redis');
const bcrypt = require('bcrypt');

// SECURE: Configure Redis client for session storage with TTL
const redisClient = redis.createClient({
  host: process.env.REDIS_HOST,
  port: process.env.REDIS_PORT
});

app.use(session({
  store: new RedisStore({
    client: redisClient,
    // SECURE: Server-side TTL for sessions
    ttl: 1800  // 30 minutes in seconds
  }),
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: false,
  rolling: true,  // SECURE: Sliding expiration - resets on each request
  cookie: {
    maxAge: 1800000,  // SECURE: 30 minute idle timeout (ms)
    httpOnly: true,   // SECURE: Prevent XSS access
    secure: true,     // SECURE: HTTPS only
    sameSite: 'strict'  // SECURE: CSRF protection
  }
}));

// SECURE: Track session creation time for absolute timeout
app.use((req, res, next) => {
  if (req.session && !req.session.createdAt) {
    req.session.createdAt = Date.now();
  }
  
  // SECURE: Enforce absolute session lifetime (12 hours)
  const ABSOLUTE_TIMEOUT = 12 * 60 * 60 * 1000;  // 12 hours in ms
  if (req.session && req.session.createdAt) {
    const sessionAge = Date.now() - req.session.createdAt;
    if (sessionAge > ABSOLUTE_TIMEOUT) {
      // SECURE: Destroy session after absolute timeout
      req.session.destroy();
      return res.status(401).json({ 
        error: 'Session expired',
        reason: 'Maximum session lifetime exceeded' 
      });
    }
  }
  
  // SECURE: Update last activity timestamp
  if (req.session && req.session.userId) {
    req.session.lastActivity = Date.now();
  }
  
  next();
});

app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  const user = await User.findOne({ email });
  
  if (user && await bcrypt.compare(password, user.passwordHash)) {
    // SECURE: Regenerate session ID on login to prevent fixation
    req.session.regenerate((err) => {
      if (err) {
        return res.status(500).json({ error: 'Session error' });
      }
      
      req.session.userId = user._id;
      req.session.createdAt = Date.now();
      req.session.lastActivity = Date.now();
      
      res.json({ message: 'Login successful' });
    });
  } else {
    res.status(401).json({ error: 'Invalid credentials' });
  }
});

// SECURE: Remember-me with rotating tokens
app.post('/remember-me', authenticateSession, async (req, res) => {
  const user = req.user;
  
  // SECURE: Generate cryptographically strong token
  const rememberToken = crypto.randomBytes(32).toString('hex');
  const tokenHash = await bcrypt.hash(rememberToken, 10);
  
  // SECURE: Store token hash with expiration
  await User.findByIdAndUpdate(user._id, {
    rememberToken: tokenHash,
    rememberTokenExpiry: Date.now() + (30 * 24 * 60 * 60 * 1000)  // 30 days
  });
  
  // SECURE: Set cookie with explicit maxAge
  res.cookie('remember_me', rememberToken, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 30 * 24 * 60 * 60 * 1000  // 30 days
  });
  
  res.json({ message: 'Remember me enabled' });
});

// Middleware to restore session from remember-me token
app.use(async (req, res, next) => {
  if (!req.session.userId && req.cookies.remember_me) {
    const token = req.cookies.remember_me;
    
    // Find user with valid remember token
    const users = await User.find({
      rememberTokenExpiry: { $gt: Date.now() }
    });
    
    for (const user of users) {
      if (await bcrypt.compare(token, user.rememberToken)) {
        // SECURE: Rotate the remember token on use
        const newToken = crypto.randomBytes(32).toString('hex');
        const newTokenHash = await bcrypt.hash(newToken, 10);
        
        await User.findByIdAndUpdate(user._id, {
          rememberToken: newTokenHash,
          rememberTokenExpiry: Date.now() + (30 * 24 * 60 * 60 * 1000)
        });
        
        // SECURE: Issue new cookie with rotated token
        res.cookie('remember_me', newToken, {
          httpOnly: true,
          secure: true,
          sameSite: 'strict',
          maxAge: 30 * 24 * 60 * 60 * 1000
        });
        
        // Create new session
        req.session.userId = user._id;
        req.session.createdAt = Date.now();
        break;
      }
    }
  }
  next();
});

// SECURE: Password change invalidates all sessions
app.post('/change-password', authenticateSession, async (req, res) => {
  const { currentPassword, newPassword } = req.body;
  const user = await User.findById(req.session.userId);
  
  const valid = await bcrypt.compare(currentPassword, user.passwordHash);
  if (!valid) {
    return res.status(401).json({ error: 'Current password incorrect' });
  }
  
  const newHash = await bcrypt.hash(newPassword, 12);
  
  // SECURE: Increment session version to invalidate old sessions
  await User.findByIdAndUpdate(user._id, {
    passwordHash: newHash,
    sessionVersion: (user.sessionVersion || 0) + 1,
    rememberToken: null,  // SECURE: Invalidate remember-me
    rememberTokenExpiry: null
  });
  
  // SECURE: Destroy current session (user will need to re-login)
  req.session.destroy();
  
  res.json({ message: 'Password changed. Please login again.' });
});

// SECURE: Logout with explicit session destruction
app.post('/logout', authenticateSession, (req, res) => {
  const userId = req.session.userId;
  
  // SECURE: Destroy server-side session
  req.session.destroy((err) => {
    if (err) {
      return res.status(500).json({ error: 'Logout failed' });
    }
    
    // SECURE: Clear remember-me cookie
    res.clearCookie('remember_me');
    res.clearCookie('connect.sid');  // Session cookie
    
    res.json({ message: 'Logged out successfully' });
  });
});

// SECURE: Require recent authentication for sensitive operations
const requireRecentAuth = (req, res, next) => {
  const RECENT_AUTH_WINDOW = 5 * 60 * 1000;  // 5 minutes
  
  if (!req.session.lastAuthTime) {
    return res.status(403).json({ 
      error: 'Recent authentication required',
      needsReauth: true 
    });
  }
  
  const timeSinceAuth = Date.now() - req.session.lastAuthTime;
  if (timeSinceAuth > RECENT_AUTH_WINDOW) {
    return res.status(403).json({ 
      error: 'Recent authentication required',
      needsReauth: true 
    });
  }
  
  next();
};

// SECURE: Step-up authentication endpoint
app.post('/reauth', authenticateSession, async (req, res) => {
  const { password } = req.body;
  const user = await User.findById(req.session.userId);
  
  if (await bcrypt.compare(password, user.passwordHash)) {
    req.session.lastAuthTime = Date.now();
    res.json({ message: 'Re-authenticated successfully' });
  } else {
    res.status(401).json({ error: 'Invalid password' });
  }
});

// SECURE: Sensitive operations require recent auth
app.post('/billing/update', authenticateSession, requireRecentAuth, async (req, res) => {
  // Update payment method - only allowed with recent authentication
  const { cardNumber, exp, cvv } = req.body;
  
  await User.findByIdAndUpdate(req.session.userId, {
    paymentMethod: { cardNumber, exp }
  });
  
  res.json({ message: 'Payment method updated' });
});

app.post('/security/mfa-disable', authenticateSession, requireRecentAuth, async (req, res) => {
  // Disable MFA - requires recent authentication
  await User.findByIdAndUpdate(req.session.userId, {
    mfaEnabled: false,
    mfaSecret: null
  });
  
  res.json({ message: 'MFA disabled' });
});

Discovery

This vulnerability is discovered by logging in, capturing the session token, waiting beyond expected timeout periods, and observing that the session remains valid indefinitely or for excessively long periods without re-authentication.

  1. 1. Session longevity baseline test

    http

    Action

    Create fresh session and test immediate access to establish baseline

    Request

    GET https://app.example.com/dashboard
    Headers:
    Cookie: session=<FRESH_SESSION>

    Response

    Status: 200
    Body:
    {
      "note": "200 OK with full access to authenticated resources"
    }

    Artifacts

    http_response_status session_data
  2. 2. Long-term session validity test

    http

    Action

    Test session validity after extended period (days/weeks) without activity

    Request

    GET https://app.example.com/account
    Headers:
    Cookie: session=<WEEKS_OLD_SESSION>

    Response

    Status: 200
    Body:
    {
      "note": "Session still valid weeks later, no expiration or idle timeout enforced"
    }

    Artifacts

    http_response_status session_age last_activity_timestamp
  3. 3. Idle timeout test

    http

    Action

    Test if sessions expire after period of inactivity

    Request

    GET https://app.example.com/api/user
    Headers:
    Cookie: session=<INACTIVE_SESSION>

    Response

    Status: 200
    Body:
    {
      "note": "Session remains valid despite 48+ hours of inactivity"
    }

    Artifacts

    session_validity idle_duration server_session_ttl
  4. 4. Remember-me token persistence test

    http

    Action

    Test persistence of remember-me tokens and whether they rotate on use

    Request

    GET https://app.example.com/dashboard
    Headers:
    Cookie: remember_token=<REMEMBER_TOKEN>

    Response

    Status: 200
    Body:
    {
      "note": "Remember token valid for months, never rotates, provides full access"
    }

    Artifacts

    remember_token_age token_rotation_status session_restoration

Exploit steps

An attacker exploits this by stealing or intercepting session tokens (through XSS, network sniffing, or physical access) and using them for extended periods to maintain unauthorized access, even after the legitimate user has logged out.

  1. 1. Long-term account access via stolen session

    Reuse intercepted session cookie

    http

    Action

    Access account using session cookie stolen weeks ago

    Request

    GET https://app.example.com/dashboard
    Headers:
    Cookie: session=<STOLEN_OLD_SESSION>

    Response

    Status: 200
    Body:
    {
      "note": "Full account access despite session age and victim password changes"
    }

    Artifacts

    account_data_access session_validity_confirmation user_activity_logs
  2. 2. Sensitive operations without re-authentication

    Modify payment information

    http

    Action

    Change billing details using old session without recent auth verification

    Request

    POST https://app.example.com/billing/update
    Headers:
    Cookie: session=<OLD_SESSION>
    Content-Type: application/json
    Body:
    {
      "card_number": "4111111111111111",
      "exp": "12/25",
      "cvv": "123"
    }

    Response

    Status: 200
    Body:
    {
      "note": "Payment method updated without step-up authentication"
    }

    Artifacts

    payment_method_change billing_records fraud_indicators
  3. 3. Data exfiltration via persistent session

    Extract sensitive data over time

    http

    Action

    Use long-lived session to systematically extract user data and documents

    Request

    GET https://app.example.com/api/documents/export
    Headers:
    Cookie: session=<PERSISTENT_SESSION>

    Response

    Status: 200
    Body:
    {
      "note": "Complete data export successful using months-old session"
    }

    Artifacts

    exported_documents pii_data download_logs
  4. 4. Account takeover via remember-me abuse

    Exploit non-rotating remember token

    http

    Action

    Use captured remember-me token for permanent access across password changes

    Request

    GET https://app.example.com/restore-session
    Headers:
    Cookie: remember_token=<CAPTURED_TOKEN>

    Response

    Status: 200
    Body:
    {
      "note": "New full session created despite victim changing password"
    }

    Artifacts

    new_session_created remember_token_reuse account_takeover_proof

Specific Impact

The attacker maintains persistent access and can modify billing and MFA settings. Incident response is harder because the same cookie continues to work until manual invalidation.

Users may be billed fraudulently and trust is damaged, leading to support load and potential regulatory reporting.

Fix

Introduce a 30-minute sliding idle timeout with a 12-hour absolute cap. Ensure the server-side store enforces TTLs and that logout and password changes revoke tokens. Add recent-auth checks for high-risk actions and set secure cookie flags.

Detect This Vulnerability in Your Code

Sourcery automatically identifies session expiration vulnerabilities and many other security issues in your codebase.

Scan Your Code for Free