How PHP Sessions Work

HTTP is a stateless protocol. Each request from a browser to a server is independent. The server has no built-in way to know that a request from user A is related to a previous request from the same user A. Sessions solve this problem by attaching a unique identifier to each user and using that identifier to track their state across requests.

When a user visits a PHP site for the first time, PHP generates a unique session ID and sends it to the browser as a cookie. By default, the cookie is called PHPSESSID. On every subsequent request, the browser sends the session ID cookie back to the server. PHP looks up the session data associated with that ID and makes it available in the $_SESSION superglobal array.

session_start();

$_SESSION['user_id'] = 42;
$_SESSION['login_time'] = time();

This is the basic mechanism. Everything else about sessions is about making this mechanism secure.

Starting Sessions and Session Configuration

Call session_start() at the very top of any PHP file that needs session access. No output can be sent to the browser before calling it, including whitespace. Configure session behaviour in php.ini or using ini_set() at runtime.

session_start();

These settings together make session cookies significantly harder to steal or misuse. The cookie_httponly flag prevents cross-site scripting attacks from reading the session cookie. The cookie_secure flag ensures the cookie is only transmitted over encrypted connections. The cookie_samesite flag prevents the cookie from being sent on cross-origin requests.

ini_set('session.cookie_httponly', 1);
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Strict');

When deploying on a server, it is worth checking that your web server configuration also enforces HTTPS. In many cases, the web server handles the Secure flag automatically when HTTPS is enabled, but verifying this during your Apache security configuration helps avoid accidental exposure.

Session Fixation: What It Is and How to Prevent It

Session fixation is an attack where the attacker sets a user's session ID before the user logs in. If the application accepts the session ID from the URL or a cookie without regenerating it on login, the attacker knows the session ID and can hijack the authenticated session.

The attack works in stages. First, the attacker obtains a valid session ID by visiting the site. Then they trick the victim into using that same session ID, for example by sending a link like https://example.com/login?SID=abc123. If the application does not regenerate the session ID on login, both the attacker and the victim share the same session. When the victim logs in, the attacker can use the same session ID to access the authenticated account.

The fix is straightforward: regenerate the session ID after any authentication event.

session_start();

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $user = validateCredentials($_POST['email'], $_POST['password']);

    if ($user) {
        session_regenerate_id(true);
        $_SESSION['user_id'] = $user['id'];
        $_SESSION['authenticated'] = true;
        header('Location: /dashboard');
        exit;
    }
}

Always call session_regenerate_id(true) before setting any authentication-related session variables. The true parameter deletes the old session file, preventing the attacker from using the old ID even if they obtained it. This single call prevents most session fixation attacks from succeeding.

Beyond login, consider regenerating the session ID after any privilege level change, such as when a user upgrades their account or accesses admin features. Each regeneration breaks the link between the old and new session, keeping the attack surface small.

Session Hijacking and Defences

Session hijacking is when an attacker obtains a valid session ID and uses it to impersonate the user. Methods include network sniffing, cross-site scripting, and accessing session data on the server.

The defences outlined above address most attack vectors. HttpOnly cookies prevent JavaScript access, Secure ensures the cookie travels only over HTTPS, and SameSite prevents cross-site request forgery. These three settings alone block the majority of session theft attempts.

For additional protection, validate that the session is being used consistently. Store a fingerprint of the user agent in the session and verify it on each request.

session_start();

$expectedFingerprint = md5($_SERVER['HTTP_USER_AGENT'] ?? '');
$storedFingerprint = $_SESSION['fingerprint'] ?? '';

if ($storedFingerprint === '') {
    $_SESSION['fingerprint'] = $expectedFingerprint;
} elseif ($storedFingerprint !== $expectedFingerprint) {
    session_destroy();
    header('Location: /login');
    exit;
}

This detects sessions used from different browsers or devices. Treat mismatches as suspicious events and require re-authentication rather than immediately destroying the session, to avoid locking out legitimate users on network changes or when switching between browsers.

Secure Session Storage

PHP stores session files by default in a writable directory accessible by the web server. On shared hosting, other users on the same server may be able to read session files. Use a custom session save path outside the web root to limit exposure.

session_save_path('/var/www/sessions');
mkdir('/var/www/sessions', 600, true);

Setting the directory permissions to 600 ensures only the web server user can read or write session files. This matters most on shared hosting environments where multiple websites run under different system users.

For applications with multiple servers, store sessions in a database or Redis rather than the filesystem. This ensures all servers access the same session data without relying on filesystem synchronisation.

ini_set('session.save_handler', 'redis');
ini_set('session.save_path', 'tcp://redis-server:6379?database=0');

When evaluating your server configuration for production use, reviewing the Ubuntu server security hardening steps can help ensure the underlying server is configured to protect session data at the operating system level.

Logging Users Out Securely

Logging out should destroy the session completely. Many applications only unset the user ID from the session but leave the session file and ID intact. This leaves the session vulnerable to reuse if an attacker gains access to the cookie.

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();

This clears the session array, deletes the session cookie from the browser, and destroys the session file on the server. The time calculation time() - 42000 sets the cookie expiry in the past, which tells the browser to remove it.

CSRF Protection with Sessions

Cross-Site Request Forgery exploits the fact that browsers automatically send session cookies with every request to a site. An attacker can trigger a state-changing request on your site if the victim has an active session and visits a malicious page.

The standard CSRF defence uses a token: a secret value stored in the session and included in forms.

session_start();

if (empty($_SESSION['csrf_token'])) {
    $_SESSION['csrf_token'] = bin2hex(random_bytes(32));
}

Add the token to every form as a hidden field.

<form method="POST" action="/update-profile">
    <input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token']); ?>">
    <input type="text" name="email">
    <button type="submit">Update</button>
</form>

Validate on submission.

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (!hash_equals($_SESSION['csrf_token'] ?? '', $_POST['csrf_token'] ?? '')) {
        http_response_code(403);
        exit('Invalid CSRF token');
    }

    // Process the form ...
}

The hash_equals function prevents timing attacks by ensuring the comparison takes the same amount of time regardless of where the mismatch occurs.

Session Expiry and Idle Timeouts

Long-lived sessions are a security risk. Implement both absolute timeouts and idle timeouts to reduce the window during which a stolen session can be used.

session_start();

// Absolute timeout: expire session after 2 hours
if (isset($_SESSION['created_at']) && $_SESSION['created_at'] < time() - 7200) {
    session_destroy();
    header('Location: /login');
    exit;
}

// Idle timeout: expire after 15 minutes of inactivity
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > 900)) {
    session_destroy();
    header('Location: /login');
    exit;
}

$_SESSION['last_activity'] = time();

Store created_at when the session is first created. Update last_activity on every request. This gives you two layers of session expiry without false positives from normal usage.

The absolute timeout sets a hard limit on session lifetime regardless of activity. The idle timeout handles forgotten sessions and users who leave their computers unattended.

Regenerating Session IDs Periodically

Even without authentication events, regenerating session IDs periodically reduces the risk from long-lived sessions. This limits how long a stolen session ID remains valid.

session_start();

// Regenerate ID every 30 minutes
if (!isset($_SESSION['last_regeneration'])) {
    $_SESSION['last_regeneration'] = time();
} elseif (time() - $_SESSION['last_regeneration'] > 1800) {
    session_regenerate_id(true);
    $_SESSION['last_regeneration'] = time();
}

Combining this with the expiry checks above creates a defence-in-depth approach. Even if one control fails, others remain in place.

Common Session Security Mistakes

Several common mistakes weaken session security even when other measures are in place.

  • Accepting session IDs from GET parameters: URLs appear in server logs, referrer headers, and browser history. If your application accepts SID in the URL, anyone who sees that URL can reuse the session.
  • Not setting all cookie flags: Leaving out HttpOnly, Secure, or SameSite weakens the protection each flag provides. Check your configuration on every deployment.
  • Keeping sessions alive indefinitely: Without timeouts, an abandoned logged-in session remains valid forever. This is especially risky for admin accounts.
  • Using predictable session IDs: PHP generates IDs using a cryptographically secure random generator by default. Using a custom session ID generator that relies on weaker randomness can undermine the entire system.

Reviewing your application for these issues is worth doing periodically, particularly when adding new features that affect authentication or user state.

When Session Configuration Needs a Review

Session security configuration should be reviewed when moving to a new server, changing hosting providers, updating PHP versions, or adding new authentication features. Each change can affect how session cookies behave or where session data is stored.

If you are building a new application, setting these configurations from the start avoids retrofitting security controls later. If you are maintaining an existing codebase, testing session behaviour across different browsers and network conditions helps identify edge cases before they become problems.

For applications that handle sensitive data or require elevated privileges, consider adding multi-factor authentication alongside strong session management. Session security and multi-factor authentication work together rather than replacing each other.

Building Secure PHP Session Handling

PHP sessions provide a straightforward foundation for maintaining user state, but the default configuration leaves gaps that attackers can exploit. Setting the HttpOnly, Secure, and SameSite flags on session cookies is the quickest way to close most of those gaps. Regenerating session IDs on authentication, implementing proper logout handling, adding CSRF tokens, and enforcing session expiry complete the picture.

These measures do not guarantee absolute security. Security depends on the full setup, including server configuration, hosting environment, application architecture, and ongoing maintenance. Reviewing session handling as part of regular security checks helps catch configuration drift or new vulnerabilities before they are exploited.

If you need help reviewing your current session configuration or implementing these measures in an existing application, prepare details about your PHP version, hosting setup, and any existing authentication code before getting in touch.