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 .env is 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 for the first time PHP creates a session:

  1. PHP generates a random session ID a long cryptographically random string
  2. PHP stores that session ID in a cookie on the user’s browser by default called PHPSESSID
  3. PHP stores session data on the server keyed by that session ID
  4. On every subsequent request the browser sends the session cookie automatically
  5. PHP reads the session ID from the cookie, looks up the server-side data, 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 without SSL 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:

Some PHP applications pass session IDs in URLs:

https://yoursite.com/dashboard?PHPSESSID=abc123def456

URL-based session IDs appear in server logs, browser history, and referrer headers when users click external links. 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

Session fixation is subtler than hijacking. The attacker does not steal a session ID they force the victim to use a session ID the attacker already knows.

The attack sequence:

  1. Attacker visits your application and obtains a valid session ID: abc123
  2. Attacker tricks the victim into using that same session ID by sending them a crafted URL: https://yoursite.com/login?PHPSESSID=abc123
  3. Victim logs in using session ID abc123
  4. If your application does not generate a new session ID after login, session abc123 is now associated with the victim's authenticated account
  5. 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.

The fix is one function call but it is one of the most commonly missed security controls in PHP applications.

PHP Session Configuration A Production Baseline

PHP’s defaults provide a baseline, but production applications should explicitly review and harden session configuration rather than relying blindly on defaults. These settings should be reviewed for every production PHP application:

; 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 single most important setting:

session.use_only_cookies = 1

This prevents PHP from accepting session IDs passed in URLs. Combined with session.use_strict_mode = 1 which rejects session IDs that PHP did not create 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 in session_regenerate_id(true) deletes the old session data on the server. The security recommendation is to regenerate the session ID after authentication and invalidate old session state where appropriate this removes the window an attacker could exploit if they had the previous session ID.

Also regenerate on privilege changes:

// Any time a user's privileges change — not just on login
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]);

Without this any XSS vulnerability in your application can steal session cookies.

Secure — HTTPS only:

session_set_cookie_params(['secure' => true]);

Without this the session cookie travels over unencrypted HTTP connections where it can be intercepted.

SameSite — controls cross-site cookie sending:

session_set_cookie_params(['samesite' => 'Lax']);

Lax sends the cookie with top-level navigation but not with embedded cross-site requests. This provides meaningful CSRF protection while preserving most legitimate use cases.

Strict never sends the cookie with cross-site requests — strongest protection but breaks flows like clicking an external link that should land the user in an authenticated state.

None always sends the cookie only use for intentional cross-site use cases and only with the Secure attribute.

The complete secure session setup in plain PHP:

// 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 environments other tenants may be able to read your session files depending on server configuration.

Database storage:

Storing sessions in a database gives you better access control and the ability to invalidate specific sessions useful when you need to force a logout for a compromised user:

session_set_save_handler(new DatabaseSessionHandler($pdo), true);
session_start();

Redis:

For applications running across multiple servers or requiring centralized session storage, Redis is a strong choice. Database sessions are another practical production option depending on your hosting architecture and isolation requirements:

; php.ini
session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379"

Laravel Session Configuration

Laravel abstracts PHP sessions behind its own session system. The 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'),

Session encryption:

Setting 'encrypt' => true encrypts all session data using Laravel's APP_KEY. Even if an attacker gains access to your session storage they cannot read the session contents without the encryption key.

Session driver:

Never use the file driver in production on shared hosting. Use database or redis for proper access control and automatic session expiry.

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.

But if you build custom authentication or manually change privileges you need to 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 theoretically maintain a session indefinitely. Absolute timeout forces re-authentication after a fixed period regardless of activity:

// Store login time in session
$_SESSION['login_time'] = time();
// On every authenticated request
$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. Applications processing financial data or personal client information should force re-authentication more frequently.

Secure Logout

Always completely destroy the session on logout not just unset session variables:

// Plain PHP — wrong approach
unset($_SESSION['user_id']); // Session ID still valid
// Plain PHP — correct approach
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 the 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 steps 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' => true in config/session.php
  • 'http_only' => true in config/session.php
  • 'encrypt' => true in config/session.php
  • 'same_site' => 'lax' in config/session.php
  • Use database or redis session driver in production
  • Call all three logout methods on every logout
  • Never build custom authentication that skips session regeneration after login

Kriosa in Production — What It Actually Protects

The principles in this article are not hypothetical. Two production PHP applications are already running with Kriosa in front of them.

Prolify — 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 compromised session could expose customer information and disrupt business operations.

Both applications have Kriosa monitoring every incoming request before it reaches the application layer.

Where Kriosa Fits

Application-level prevention comes first. Kriosa is a detection and enforcement layer it complements secure session configuration, it does not replace it.

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. An authenticated session suddenly appearing from an unexpected location which may be a signal worth investigating, though IP and location changes alone are not proof of hijacking since VPNs, mobile networks, and corporate proxies can all cause legitimate changes. Rapid requests cycling through session ID values that may indicate probing activity.

These are behavioral signals that a security monitoring layer can detect and surface giving you visibility into what is happening before a breach occurs.

You have fixed the vulnerability. Now see what happens when someone keeps probing.

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

Documentation: kriosa Docs.

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 .env is only the beginning
  • Article 12: This article — session security in PHP and what most developers get wrong