Session Security
Why Sessions Are a Prime Target In simple terms: After a user logs in, the session ID stored in a cookie is the only thing that proves "this request comes from Asha". Whoever has that ID is Asha as fa
Why Sessions Are a Prime Target
In simple terms: After a user logs in, the session ID stored in a cookie is the only thing that proves "this request comes from Asha". Whoever has that ID is Asha as far as the application is concerned — no password needed. Session security is about keeping session IDs secret, unguessable, short-lived, and impossible to plant.
Technically: PHP sessions identify users by a random ID sent in a cookie (see $_SESSION and PHP Sessions). Attacks target that ID: stealing it (session hijacking), forcing a known ID on a victim (session fixation), or abusing an open session left behind (unattended computers, long-lived sessions). Defenses combine secure cookie settings, ID regeneration, timeouts, proper logout, and protecting the session storage.
Threats and Defenses
| Threat | How It Happens | Defense |
|---|---|---|
| Hijacking via network sniffing | The cookie travels over unencrypted HTTP | HTTPS everywhere; Secure cookie flag; HSTS |
| Hijacking via XSS | A script reads document.cookie | HttpOnly cookie flag; XSS prevention |
| Fixation | The attacker makes the victim use a session ID the attacker knows | use_strict_mode; regenerate the ID at login |
| Prediction | Guessable IDs | PHP's default random IDs (do not generate your own) |
| CSRF | Other sites use the victim's session | CSRF tokens; SameSite cookies (see CSRF Protection) |
| Abandoned sessions | Shared or stolen devices | Idle and absolute timeouts; logout; re-authentication |
| Storage exposure | Session files readable by other users or apps | Private save path or protected Redis/database |
Secure Session Configuration
<?php
function startSecureSession(): void
{
if (session_status() === PHP_SESSION_ACTIVE) {
return;
}
session_start([
'name' => '__Host-SID',
'cookie_path' => '/',
'cookie_secure' => true,
'cookie_httponly' => true,
'cookie_samesite' => 'Lax',
'use_strict_mode' => true,
'use_only_cookies' => true,
'gc_maxlifetime' => 7200,
]);
}
| Setting | Protection |
|---|---|
name => '__Host-SID' | The __Host- prefix makes browsers accept the cookie only if it is Secure, has Path=/, and no Domain — subdomains cannot set or overwrite it (requires HTTPS; use a plain name such as SID on local http:// setups) |
cookie_secure | The cookie is only sent over HTTPS |
cookie_httponly | JavaScript cannot read the cookie, so XSS cannot steal it directly |
cookie_samesite => 'Lax' | The cookie is not sent with cross-site POST requests |
use_strict_mode | PHP rejects session IDs it did not create and issues a new one — blocks most fixation attacks |
use_only_cookies | Session IDs in URLs are ignored (they leak via links, logs, and Referer headers) |
The same values can be set in php.ini (session.cookie_secure = 1, …) so that every script is protected.
Regenerating the Session ID
The Fixation Attack
1. Attacker obtains a valid session ID (e.g., visits the site: SID=abc123)
2. Attacker tricks the victim into using SID=abc123 (a crafted link or a cookie set via a vulnerable subdomain)
3. Victim logs in — the session abc123 is now authenticated
4. Attacker uses SID=abc123 and is logged in as the victim
The Fix: New ID at Every Privilege Change
<?php
session_start();
$before = session_id();
session_regenerate_id(true);
var_dump($before === session_id());
var_dump(strlen(session_id()));
Output
bool(false)
int(32)
session_regenerate_id(true) gives the session a new ID and deletes the old session data from storage (true), so an ID known to an attacker becomes worthless. Call it:
- immediately after a successful login,
- when privileges change (for example, entering an admin area or after re-authentication),
- periodically during long sessions (see PHP Sessions).
A Login and Logout Pair
<?php
function logIn(int $userId): void
{
session_regenerate_id(true);
$_SESSION['user_id'] = $userId;
$_SESSION['logged_in_at'] = time();
$_SESSION['last_activity'] = time();
$_SESSION['_csrf'] = bin2hex(random_bytes(32)); // fresh CSRF token
}
function logOut(): void
{
$_SESSION = [];
$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'],
]);
session_destroy();
}
A complete logout clears the session data in memory, deletes the cookie in the browser (with the same parameters it was set with), and destroys the stored session. Logging out should be a POST request protected by a CSRF token.
Re-Authentication for Sensitive Actions
Even with a valid session, sensitive actions — changing the email or password, viewing payment details, deleting the account — should require proof that the user is present, not just their browser:
<?php
function passwordRecentlyConfirmed(array $session, int $now, int $maxAgeSeconds = 900): bool
{
$confirmedAt = $session['password_confirmed_at'] ?? null;
return is_int($confirmedAt) && $now - $confirmedAt <= $maxAgeSeconds;
}
$now = 1_792_054_800;
var_dump(passwordRecentlyConfirmed(['password_confirmed_at' => $now - 300], $now));
var_dump(passwordRecentlyConfirmed(['password_confirmed_at' => $now - 3600], $now));
var_dump(passwordRecentlyConfirmed([], $now));
Output
bool(true)
bool(false)
bool(false)
If the password was not confirmed within the last 15 minutes, the application redirects to a "confirm your password" page, verifies the password with password_verify(), stores password_confirmed_at, and regenerates the session ID. GitHub and many banking sites use this pattern ("sudo mode").
Additional Measures
| Measure | Notes |
|---|---|
| Idle and absolute timeouts | Expire inactive sessions (e.g., 30 minutes) and all sessions after a maximum age (e.g., 8 hours); see PHP Sessions |
| Minimal session data | Store the user ID and small flags; load the user's data from the database on each request so that role changes and account locks take effect immediately |
| Server-side session list | With database-stored sessions, users can see active devices and log out everywhere; invalidate all sessions after a password change |
| Protected storage | File sessions in a directory readable only by the PHP user (not a shared /tmp on shared hosting); Redis with authentication and no public network access |
| Binding to client details | Comparing the stored User-Agent with each request can detect crude hijacking, but it is easy to spoof and IP addresses change legitimately (mobile networks); treat it as a weak signal only |
Common Mistakes
Not Regenerating the ID at Login
Effect: Session fixation attacks succeed.
Fix: Call session_regenerate_id(true) after authentication.
Serving Pages Over HTTP
Effect: Session cookies can be intercepted on public networks.
Fix: Use HTTPS for the entire site and set the Secure flag.
Incomplete Logout
Effect: The old session remains valid on the server.
Fix: Clear data, delete the cookie, and call session_destroy().
Storing Permissions Only in the Session
Effect: Revoked roles or locked accounts stay active until the session ends.
Fix: Re-check the user's status and roles from the database.
Very Long Sessions Without Re-Authentication
Effect: Stolen or abandoned sessions remain useful for a long time.
Fix: Use timeouts and require the password for sensitive actions.
Best Practices
- Configure sessions with
Secure,HttpOnly,SameSite, strict mode, and cookie-only IDs. - Regenerate the session ID on login and privilege changes.
- Implement complete logout and invalidate sessions after password changes.
- Enforce idle and absolute timeouts and re-authenticate for sensitive actions.
- Protect the session storage and keep session data minimal.
Official reference: https://www.php.net/manual/en/features.session.security.php