Session Security
Regenerate the session id after a login, enforce idle and absolute timeouts, and destroy a session properly on logout.
session_regenerate_id(bool $delete_old_session = false): boolThe session id is the only thing between a visitor and their account. Two attacks follow from that. In session fixation the attacker gets the victim to use an id the attacker already knows, then rides the same session once the victim logs in. In session hijacking the attacker steals an id that is already in use.
Change the id at every privilege change
session_regenerate_id(true) issues a fresh id and deletes the old server-side data, so an id captured before that moment is worthless. Call it straight after a successful login, after a password change, and on logout. Switching on session.use_strict_mode makes PHP reject an id it never issued, which is what actually closes fixation. It is off by default.
Two clocks
An idle timeout ends a session after a stretch with no requests. An absolute timeout ends it a fixed time after sign-in, however busy it has been. Keep last_seen and started_at in the session and check both on every request. Do not lean on the cookie's own expiry, because that copy belongs to the client.
Logging out takes three steps
Empty $_SESSION, expire the session cookie by its own name, then call session_destroy() to drop the data on the server. Miss the second step and the browser keeps presenting a dead id. Miss the third and the data is still on disk.
Example
When to use it
- Harden a login so a planted session id can never become an authenticated one.
- Sign an idle admin out after thirty minutes.
- Make logout end the session on the server instead of only hiding the page.
- Force every session to end a fixed time after sign-in, however active it is.
More examples
Regenerate the id after a login
Three lines that close session fixation. The id changes before anything is written that marks the session as signed in, so an id the attacker planted earlier never becomes an authenticated one. The two timestamps are what the idle and absolute checks read on later requests.
<?php
declare(strict_types=1);
// Inside the login handler, after password_verify() has returned true.
session_regenerate_id(true);
$_SESSION['user_id'] = $userId;
$_SESSION['started_at'] = time();
$_SESSION['last_seen'] = time();A logout that really logs out
The complete logout. session_get_cookie_params() and session_name() are used so the expiring cookie matches the one that was set, whatever the configuration says; a mismatched path leaves the original cookie in place. $_SESSION = [] and session_unset() do the same job here, so you will see either.
<?php
declare(strict_types=1);
session_start();
// 1. Empty the array in this request.
$_SESSION = [];
// 2. Tell the browser to drop the session cookie.
$params = session_get_cookie_params();
setcookie(session_name(), '', [
'expires' => time() - 3600,
'path' => $params['path'],
'domain' => $params['domain'],
'secure' => $params['secure'],
'httponly' => $params['httponly'],
'samesite' => $params['samesite'],
]);
// 3. Delete the data on the server.
session_destroy();
header('Location: /login.php', true, 303);
exit;Hardened start with both timeouts
This is the include that every signed-in page pulls in first. The options array hardens the session cookie and turns on use_strict_mode, and the block below enforces both clocks before the page does anything. last_seen is refreshed only after the checks pass, otherwise the idle timeout could never fire.
<?php
declare(strict_types=1);
session_start([
'cookie_secure' => true,
'cookie_httponly' => true,
'cookie_samesite' => 'Lax',
'use_strict_mode' => true,
]);
$now = time();
if (isset($_SESSION['user_id'])) {
$idle = $now - $_SESSION['last_seen'];
$age = $now - $_SESSION['started_at'];
if ($idle > 1800 || $age > 28800) {
$_SESSION = [];
session_destroy();
header('Location: /login.php?expired=1', true, 303);
exit;
}
$_SESSION['last_seen'] = $now;
}
Discussion