ORIGINALLY PUBLISHED ON MEDIUM
The session ID is the key to your user's account. Here is every way attackers steal it and how to stop them.
This is the twelfth article in a series on PHP and Laravel application security.
So far we have covered:
- Detecting SQL injection attempts in PHP logs
- Why URL encoding blinds most PHP security checks
- The decode bomb problem with unlimited URL decoding
- Why parameterized queries are the only real fix for SQL injection
- XSS prevention in Laravel and why
{!! !!}is the line between safe and hacked - How attackers enumerate your Laravel app before exploiting it
- File upload security — the file that isn't what it claims to be
- Path traversal in PHP — how
../escapes your application - Command injection in PHP — when
exec()becomes an attack surface - Broken access control in Laravel — why being logged in is not enough
- Secrets in Laravel — why
.envis only the beginning
Every article in this series follows the same principle: understand the attack before you try to stop it.
Session security is different from most vulnerabilities in this series. SQL injection, XSS, and path traversal attack your application directly. Session attacks attack the identity layer the mechanism that proves who a user is after they have authenticated. Get it wrong and it does not matter how secure the rest of your application is. An attacker with a valid session ID is indistinguishable from a legitimate user.
How PHP Sessions Work
When a user visits your PHP application PHP creates a session:
- PHP generates a cryptographically random session ID
- PHP stores that session ID in a cookie on the user's browser — by default called
PHPSESSID - PHP stores session data on the server keyed by that session ID
- On every subsequent request the browser sends the session cookie automatically
- PHP reads the session ID from the cookie and restores the session
The session ID is the key to everything. Whoever holds that session ID can impersonate the user it belongs to. This is what every session attack targets.
Attack 1 — Session Hijacking
Session hijacking is when an attacker steals a valid session ID and uses it to impersonate the legitimate user.
Method 1 — XSS:
If your application has an XSS vulnerability an attacker injects JavaScript that reads the session cookie and sends it to their server. This is why the HttpOnly cookie attribute exists it prevents JavaScript from reading cookies entirely.
Method 2 — Network interception:
If your application runs over HTTP an attacker on the same network can intercept the session cookie from unencrypted traffic. This is why the Secure cookie attribute exists it tells the browser to only send the cookie over HTTPS.
Method 3 — Session IDs in URLs:
https://yoursite.com/dashboard?PHPSESSID=abc123def456
URL-based session IDs appear in server logs, browser history, and referrer headers. Stealing them requires no technical attack just access to a log file.
Method 4 — Predictable session IDs:
If session IDs are generated with weak randomness an attacker can predict or brute force them. PHP's default session ID generation is cryptographically secure — but custom session ID generation in older applications often is not.
Attack 2 — Session Fixation
The attacker does not steal a session ID they force the victim to use one the attacker already knows.
The attack sequence:
- Attacker obtains a valid session ID:
abc123 - Attacker tricks the victim into using that session ID:
https://yoursite.com/login?PHPSESSID=abc123 - Victim logs in using session ID
abc123 - If your application does not generate a new session ID after login, session
abc123is now authenticated - Attacker uses session ID
abc123— which they have known all along to access the victim's account
The victim logged in successfully. The attacker never knew their password. The attack worked because the session ID did not change after authentication.
PHP Session Configuration A Production Baseline
; php.ini
; Never accept session IDs in URLs only in cookies
session.use_only_cookies = 1
; Prevent JavaScript from reading session cookies
session.cookie_httponly = 1
; Only send session cookies over HTTPS
session.cookie_secure = 1
; Restrict cookie to same-site requests
session.cookie_samesite = Lax
; Reject unrecognized session IDs
session.use_strict_mode = 1
; Set a reasonable session lifetime
session.gc_maxlifetime = 1440
The two most important settings:
session.use_only_cookies = 1 prevents PHP from accepting session IDs in URLs.
session.use_strict_mode = 1 rejects session IDs that PHP did not create.
Together these two settings eliminate the most common session fixation attack vector at the configuration level.
Fixing Session Fixation — Session Regeneration
Configuration alone is not enough. Your application must regenerate the session ID after every successful login:
session_start();
if ($credentialsAreValid) {
// Regenerate the session ID and delete the old session data
session_regenerate_id(true);
// Now set authenticated session data
$_SESSION['user_id'] = $user->id;
$_SESSION['authenticated'] = true;
}
The true parameter deletes the old session data on the server. This removes the window an attacker could exploit if they had the previous session ID.
Also regenerate on privilege changes:
session_regenerate_id(true);
$_SESSION['role'] = 'admin';
Cookie Security Attributes
Every session cookie requires three security attributes.
HttpOnly — prevents JavaScript from reading the cookie:
session_set_cookie_params(['httponly' => true]);
Secure — HTTPS only:
session_set_cookie_params(['secure' => true]);
SameSite — controls cross-site cookie sending:
session_set_cookie_params(['samesite' => 'Lax']);
Lax provides meaningful CSRF protection while preserving most legitimate use cases. Strict is strongest but breaks flows like clicking an external link into an authenticated state. None always sends the cookie only for intentional cross-site use cases and only with Secure.
The complete secure session setup:
// Call before session_start()
session_set_cookie_params([
'lifetime' => 0, // Expires when browser closes
'path' => '/',
'domain' => '',
'secure' => true, // HTTPS only
'httponly' => true, // No JavaScript access
'samesite' => 'Lax', // CSRF protection
]);
session_start();
Session Storage Security
By default PHP stores session data in files in /tmp. On shared hosting other tenants may be able to read your session files.
Database storage:
session_set_save_handler(new DatabaseSessionHandler($pdo), true);
session_start();
Redis:
session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379"
Database sessions give you the ability to invalidate specific sessions useful when you need to force a logout for a compromised account.
Laravel Session Configuration
Laravel abstracts PHP sessions behind its own session system. Configuration lives in config/session.php.
The critical production settings:
// config/session.php
// Encrypt session data at rest
'encrypt' => true,
// HTTPS only session cookie
'secure' => env('SESSION_SECURE_COOKIE', true),
// Prevent JavaScript from reading the cookie
'http_only' => true,
// CSRF protection
'same_site' => 'lax',
// Session lifetime in minutes
'lifetime' => env('SESSION_LIFETIME', 120),
// Session storage driver
'driver' => env('SESSION_DRIVER', 'redis'),
'encrypt' => true encrypts all session data using your APP_KEY. Even if an attacker gains access to your session storage they cannot read the contents without the encryption key.
Never use the file driver in production on shared hosting. Use database or redis.
Session Regeneration in Laravel
Laravel automatically regenerates the session ID on login when you use Auth::attempt() or Auth::login(). This is built into the framework.
If you build custom authentication or manually change privileges regenerate explicitly:
// Regenerate session ID — keep existing data
$request->session()->regenerate();
// Regenerate and invalidate the old session completely
$request->session()->invalidate();
$request->session()->regenerateToken();
regenerateToken() regenerates the CSRF token stored in the session. Call this alongside session regeneration after login CSRF token fixation mirrors session fixation and should be addressed at the same time.
Absolute Session Timeout
Idle timeout alone is not enough. A user who stays active can maintain a session indefinitely. Absolute timeout forces re-authentication after a fixed period regardless of activity:
$_SESSION['login_time'] = time();
$absoluteTimeout = 8 * 60 * 60; // 8 hours
if (time() - $_SESSION['login_time'] > $absoluteTimeout) {
session_destroy();
header('Location: /login?reason=timeout');
exit;
}
In Laravel:
// config/session.php
'lifetime' => 480, // 8 hours in minutes
For applications handling sensitive data consider shorter absolute timeouts.
Secure Logout
Always completely destroy the session on logout not just unset session variables:
// Wrong — session ID still valid
unset($_SESSION['user_id']);
// Correct
session_start();
$_SESSION = [];
if (ini_get('session.use_cookies')) {
$params = session_get_cookie_params();
setcookie(
session_name(), '',
time() - 42000,
$params['path'],
$params['domain'],
$params['secure'],
$params['httponly']
);
}
session_destroy();
In Laravel the correct logout sequence is three steps:
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
Each step matters:
-
Auth::logout()clears authentication state and removes the user from the session -
session()->invalidate()destroys the session and generates a new session ID -
session()->regenerateToken()regenerates the CSRF token
Missing any of these leaves the old session potentially exploitable.
The Session Security Checklist
For plain PHP:
-
session.use_only_cookies = 1— never accept session IDs in URLs -
session.use_strict_mode = 1— reject unrecognized session IDs -
session.cookie_httponly = 1— prevent JavaScript from reading cookies -
session.cookie_secure = 1— HTTPS only -
session.cookie_samesite = Lax— CSRF protection - Call
session_regenerate_id(true)after every successful login - Call
session_regenerate_id(true)after every privilege change - Destroy the session completely on logout not just unset variables
- Use database or Redis session storage in production
- Implement absolute session timeout for sensitive applications
For Laravel:
-
'secure' => trueinconfig/session.php -
'http_only' => trueinconfig/session.php -
'encrypt' => trueinconfig/session.php -
'same_site' => 'lax'inconfig/session.php - Use
databaseorredissession driver in production - Call all three logout methods on every logout
- Never build custom authentication that skips session regeneration after login
Kriosa in Production Real Applications, Real Protection
The principles in this article are not hypothetical. Two production PHP applications are already running with Kriosa in front of them.
Prooflify — a design-proofing platform with client review workflows. Designers share work with clients, clients leave feedback, and the platform manages approval states. Every client session is a trust boundary the wrong session in the wrong hands could expose a client's unreleased creative work.
belle full — a production PHP application for bakers, secured with Kriosa from day one. It handles customer orders, client data, and business operations for a real business where a session breach means a real person's data and livelihood are at risk.
Both applications have Kriosa monitoring every incoming request before it reaches the application layer.
Where Kriosa Fits
Configure your sessions correctly first. Then put Kriosa in front of your application to detect suspicious requests and behavior that your application code alone cannot see.
Session attacks leave behavioral traces before they succeed requests containing session IDs in URL parameters on applications that should only use cookies, authenticated sessions appearing from unexpected locations, rapid requests cycling through session ID values that may indicate probing activity.
These are behavioral signals a security monitoring layer can detect and surface before a breach occurs.
Kriosa sits in front of your application and gives you visibility into suspicious requests before they reach your code with the XAI dashboard explaining exactly what was detected and why it was flagged.
Try it free: kriosa.com
Install it: composer require kriosa-ai/kriosa-php
Built by a developer, for developers who want to understand their security — not just outsource it.
The Series So Far
- Article 1: What your PHP logs actually look like during a SQL injection attack
- Article 2: Why URL encoding can break PHP security checks
- Article 3: The decode bomb problem — why unlimited URL decoding can be its own vulnerability
- Article 4: Parameterized queries — the only real fix for SQL injection
-
Article 5: XSS prevention in Laravel and why
{!! !!}is the line between safe and hacked - Article 6: How attackers enumerate your Laravel app before exploiting it
- Article 7: File upload security in PHP and Laravel
-
Article 8: Path traversal in PHP — how
../escapes your application -
Article 9: Command injection in PHP — when
exec()becomes an attack surface - Article 10: Broken access control in Laravel — why being logged in is not enough
-
Article 11: Secrets in Laravel — why
.envis only the beginning - Article 12: This article — session security in PHP and what most developers get wrong
Top comments (0)