Timing attacks and hash_equals() for constant-time comparison
Concept
Sessions are the standard mechanism for maintaining state across HTTP requests. PHP sessions store data server-side, identified by a session ID stored in a cookie. Misconfigured sessions lead to session fixation, session hijacking, and session theft.
Session fixation: Attacker gives victim a known session ID (e.g., via URL parameter). After victim logs in, attacker uses the same ID to take over the authenticated session. Fix: call session_regenerate_id(true) on every authentication state change.
Session hijacking: Attacker obtains a valid session cookie (via XSS, network sniffing, server logs). Fix: HTTPS (prevents sniffing), HttpOnly flag (prevents XSS cookie theft), short session lifetimes, IP/user-agent binding (optional — breaks mobile users).
Critical session security settings:
session.cookie_httponly = 1: JavaScript cannot access the session cookie — blocks XSS-based cookie theft.session.cookie_secure = 1: Cookie only sent over HTTPS — prevents transmission over HTTP.session.cookie_samesite = Lax(PHP 7.3+): Prevents CSRF via cookie.session.use_only_cookies = 1: Prevents session ID from being passed in URL — prevents log exposure.session.use_strict_mode = 1: PHP rejects session IDs not generated by the server — prevents session fixation.session.gc_maxlifetime = 1440: Sessions expire after 24 minutes of inactivity.
Laravel session: Configured in config/session.php. Default driver is file. Production: use redis or database. Session data is encrypted at rest (APP_KEY used for encryption).
Code Example
<?php
// Secure session configuration
ini_set('session.cookie_httponly', '1');
ini_set('session.cookie_secure', '1'); // requires HTTPS
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.use_only_cookies', '1');
ini_set('session.use_strict_mode', '1');
ini_set('session.gc_maxlifetime', '1440');
session_start();
// Prevent session fixation — regenerate ID on login
function loginUser(array $credentials): bool
{
$user = authenticate($credentials);
if (!$user) return false;
// CRITICAL: regenerate session ID after authentication
// true = delete the old session data
session_regenerate_id(true);
$_SESSION['user_id'] = $user['id'];
$_SESSION['authenticated'] = true;
$_SESSION['login_time'] = time();
return true;
}
// Session timeout enforcement
function checkSessionTimeout(int $maxIdleSeconds = 1800): void
{
if (isset($_SESSION['last_activity'])) {
if (time() - $_SESSION['last_activity'] > $maxIdleSeconds) {
session_unset();
session_destroy();
// Redirect to login
header('Location: /login?reason=timeout');
exit;
}
}
$_SESSION['last_activity'] = time();
}
// Logout — destroy everything
function logout(): void
{
session_unset();
session_destroy();
// Clear the cookie
setcookie(session_name(), '', [
'expires' => time() - 3600,
'path' => '/',
'secure' => true,
'httponly' => true,
'samesite' => 'Lax',
]);
session_regenerate_id(true);
}
// Laravel session config (config/session.php)
// 'driver' => env('SESSION_DRIVER', 'redis'),
// 'lifetime' => 120, // minutes
// 'expire_on_close' => false,
// 'encrypt' => true, // encrypt session data
// 'secure' => true, // HTTPS only
// 'http_only' => true, // no JS access
// 'same_site' => 'lax',