DEV Community

Cover image for Session Security in PHP — What Most Developers Get Wrong

Session Security in PHP — What Most Developers Get Wrong

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 .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 PHP creates a session:

  1. PHP generates a cryptographically random session ID
  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 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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Attacker obtains a valid session ID: abc123
  2. Attacker tricks the victim into using that session ID: 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 authenticated
  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.

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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';
Enter fullscreen mode Exit fullscreen mode

Cookie Security Attributes

Every session cookie requires three security attributes.

HttpOnly — prevents JavaScript from reading the cookie:

session_set_cookie_params(['httponly' => true]);
Enter fullscreen mode Exit fullscreen mode

Secure — HTTPS only:

session_set_cookie_params(['secure' => true]);
Enter fullscreen mode Exit fullscreen mode

SameSite — controls cross-site cookie sending:

session_set_cookie_params(['samesite' => 'Lax']);
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

Redis:

session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379"
Enter fullscreen mode Exit fullscreen mode

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'),
Enter fullscreen mode Exit fullscreen mode

'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();
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

In Laravel:

// config/session.php
'lifetime' => 480, // 8 hours in minutes
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

In Laravel the correct logout sequence is three steps:

Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)