Session Security
A session id is effectively a password that gets sent on every request instead of typed once. Whoever holds a valid session id can act as the user it belongs to, without ever knowing their real password. Session security is entirely about making that id hard to steal, hard to guess, and hard to reuse once it should no longer be valid. This page covers the two attacks every PHP developer should understand — fixation and hijacking — and the concrete settings and function calls that defend against them.
Session fixation
In a fixation attack, the attacker does not steal an id — they hand the victim one. If an attacker can get a victim to visit the site using a session id the attacker already knows (for example through a crafted link, or because the id was accepted from a URL parameter), and the victim then logs in under that same id, the attacker's browser is now logged in too, because it holds the identical id. The defense is simple: never keep using the session id that existed before login. Generate a brand-new one the moment a visitor's privilege level changes.
Session hijacking
Hijacking is the more familiar attack: the attacker obtains a valid, already-authenticated session id — sniffed off an insecure network, read from an XSS-injected script, or leaked through a referrer header — and replays it to impersonate the victim. Unlike fixation, hijacking happens after login, so it cannot be solved by regenerating the id at login time alone. It needs the transport itself (HTTPS), the cookie flags, and some detection of "this looks like a different client than before" working together.
Regenerate the session id after login
session_regenerate_id(true) issues a new session id, copies the existing $_SESSION data over to it, and — because of the true argument — deletes the old session's data on the server so the previous id can no longer be used at all. This single call is the primary defense against fixation and should run immediately after any point where a visitor gains new privileges: logging in, switching accounts, or being promoted to an admin role.
Regenerating the session id after a successful login
<?php session_start(); // ... verify username + password_verify() succeeded ... session_regenerate_id(true); // new id, old session data destroyed server-side $_SESSION['user_id'] = $user['id']; $_SESSION['logged_in_at'] = time();
Locking down the session cookie
The session id is only as safe as the cookie carrying it. Three php.ini directives (or their runtime equivalents via session_set_cookie_params()) control how that cookie behaves.
Setting | Effect |
|---|---|
session.cookie_httponly | When on, JavaScript cannot read the session cookie via |
session.cookie_secure | When on, the browser only ever sends the session cookie over HTTPS, so it cannot leak in cleartext on an insecure network. |
session.cookie_samesite | Set to |
Setting secure cookie params before session_start()
<?php
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => '',
'secure' => true, // only over HTTPS
'httponly' => true, // not readable from JavaScript
'samesite' => 'Lax', // not sent on most cross-site requests
]);
session_start();Binding a session to IP and User-Agent
An extra layer some applications add is recording the visitor's IP address and User-Agent header alongside the session data at login, then comparing them on every later request. If either value changes mid-session, the application can treat it as suspicious and force a re-login. This does raise the bar for an attacker who has only stolen the session id itself, since they would also need to spoof a matching IP and User-Agent.
A simple (imperfect) binding check
<?php
session_start();
if (!isset($_SESSION['ip']) || !isset($_SESSION['ua'])) {
$_SESSION['ip'] = $_SERVER['REMOTE_ADDR'];
$_SESSION['ua'] = $_SERVER['HTTP_USER_AGENT'] ?? '';
} elseif (
$_SESSION['ip'] !== $_SERVER['REMOTE_ADDR']
|| $_SESSION['ua'] !== ($_SERVER['HTTP_USER_AGENT'] ?? '')
) {
session_unset();
session_destroy();
// force the visitor to log in again
}session_regenerate_id(true)immediately after login (and after any other privilege change) defeats fixation.Set
httponly,secure, andsamesiteon the session cookie so it cannot be read by scripts, sent in the clear, or replayed cross-site.Serve the whole site over HTTPS —
cookie_secureis meaningless without it.Treat IP/User-Agent binding as a tripwire, not a guarantee, and expect some false positives.
Always give logged-in users a real way to log out that both destroys the server-side session and clears the cookie.