Security Resources

⌘K
  1. Home
  2. Security Resources
  3. Secure Code Explain
  4. Missing Session Expiry and Logout Invalidation

Missing Session Expiry and Logout Invalidation

Here is an example of PHP code that is vulnerable to sessions that never expire and a logout that does not invalidate them:

🥺 Vulnerable Code

<?php
session_start();

if (password_verify($_POST['password'], $user['hash'])) {
    // Vulnerable: the pre-login session id is reused and never expires
    $_SESSION['user_id'] = $user['id'];
    setcookie('remember', $user['id'], time() + 60 * 60 * 24 * 365);
}

function logout() {
    // Only the client-side value is dropped - the server session stays valid
    unset($_SESSION['user_id']);
}

Three problems stack up here. The session id from before authentication is kept, which is the session fixation pattern. There is no idle or absolute timeout, so a cookie copied off a shared laptop still works months later. And logout only clears one key in the array: the session file lives on, and the remember cookie is a plain user id that anyone can edit to impersonate another account.

😎 Secure Code

Here is a version of the same code that is secured against sessions that never expire and a logout that does not invalidate them:

<?php
session_set_cookie_params([
    'lifetime' => 0,
    'path'     => '/',
    'secure'   => true,
    'httponly' => true,
    'samesite' => 'Lax',
]);
session_start();

if (password_verify($_POST['password'], $user['hash'])) {
    session_regenerate_id(true);              // new id on privilege change
    $_SESSION['user_id']    = $user['id'];
    $_SESSION['created_at'] = time();
    $_SESSION['last_seen']  = time();
}

function enforce_session_limits(): void {
    $absolute = 8 * 3600;   // hard cap on session lifetime
    $idle     = 30 * 60;    // inactivity cap

    if (time() - $_SESSION['created_at'] > $absolute
        || time() - $_SESSION['last_seen'] > $idle) {
        logout();
        header('Location: /login?expired=1');
        exit;
    }

    $_SESSION['last_seen'] = time();
}

function logout(): void {
    $_SESSION = [];
    session_destroy();                        // server-side record removed
    setcookie(session_name(), '', time() - 3600, '/', '', true, true);
}

Regenerating the id at login kills fixation, the idle and absolute limits bound how long a stolen cookie is useful, and session_destroy() plus an expired cookie means logout actually ends the session on both sides. If you offer remember-me, store a random high-entropy selector and a hashed verifier server-side, rotate it on every use, and give users a way to see and revoke their active sessions.