SEC-002
Session cookie settings that survive a security review
Five flags and one call to regenerate. Defaults leave the session cookie readable by scripts and sendable over plain HTTP.
PHP's session defaults were set when the web looked different. Out of the box the session cookie is reachable from JavaScript, will travel over an unencrypted connection, and keeps the same identifier from before login to after it.
That last one is session fixation: an attacker who can set a cookie in the victim's browser before they sign in ends up holding a session that becomes authenticated when the victim logs in. Regenerating the id at the moment privilege changes closes it, and it is one line.
Set the parameters before starting the session. Calling session_set_cookie_params after session_start does nothing, silently, which is why so many sites have this code and none of its effects.
function secure_session_start(string $name = 'app'): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
return;
}
$https = (($_SERVER['HTTPS'] ?? '') !== '' && $_SERVER['HTTPS'] !== 'off')
|| (($_SERVER['HTTP_X_FORWARDED_PROTO'] ?? '') === 'https');
// Must come BEFORE session_start(), or it does nothing at all.
session_set_cookie_params([
'lifetime' => 0, // dies with the browser session
'path' => '/',
'domain' => '', // exact host only, no subdomain sharing
'secure' => $https, // never sent over plain http
'httponly' => true, // unreachable from JavaScript
'samesite' => 'Lax', // survives normal navigation, blocks cross-site POST
]);
session_name($name);
ini_set('session.use_strict_mode', '1'); // reject session ids we never issued
session_start();
}
function login_user(int $userId): void
{
// New id at the moment privilege changes. This is the fixation fix.
session_regenerate_id(true);
$_SESSION['uid'] = $userId;
$_SESSION['ua'] = substr((string) ($_SERVER['HTTP_USER_AGENT'] ?? ''), 0, 120);
}
Using it
use_strict_mode is the underrated one. Without it PHP will happily adopt any session id the browser presents, including one the attacker chose.
Regenerate on logout as well as login, and destroy the session data rather than just clearing your own keys.
What bites people
SameSite=Lax breaks flows where a third party POSTs back into your site, which includes some payment callbacks. Those endpoints should not be reading a session anyway — verify them by signature instead of loosening the cookie.
Setting secure to true on a site that is not fully HTTPS logs everyone out silently. Detect it rather than hardcoding it, and fix the HTTPS.