Session Fixation: When the Attacker Sets the Session ID Before You Log In

Session fixation happens when an attacker can set or influence the session ID that a victim receives before they authenticate. The attack sequence is straightforward. The attacker visits the target site and receives a session ID from PHP, something like PHPSESSID=a3f8b21c9d3e4f7a6b2c8e1d0f9a2b5c. The attacker then sends the victim a link to the target site that includes this session ID as a parameter: https://example.com/login?PHPSESSID=a3f8b21c9d3e4f7a6b2c8e1d0f9a2b5c.

The victim clicks the link, visits the site, and logs in. Here is the critical part: the application does not regenerate the session ID on login. It keeps using the session ID the attacker already knows. Both the attacker and the victim now share the same session. When the victim authenticates, the attacker automatically has an authenticated session with the same ID.

The fix is a single function call that must happen immediately after every successful authentication, before any content that requires authentication is sent to the browser:

session_regenerate_id(true);

The true argument destroys the old session file. Without it, PHP keeps both sessions active and the old session data, including the pre-authentication state, remains accessible. Using session_regenerate_id(false) is a common mistake that provides a false sense of security while leaving the fixation vector open. The regeneration must happen immediately after the authentication check succeeds, before any output is sent.

Session fixation is especially dangerous in applications that accept session IDs from URL parameters. If your application reads session IDs from GET parameters, the attack above works trivially. This is why cookie-based sessions are significantly safer than URL-based sessions. PHP's default configuration uses cookies, but applications can be configured to fall back to URL session IDs, and some older or poorly written applications do exactly that.

Beyond login, session regeneration should also happen at privilege transitions. When a regular user escalates to admin access, regenerate the session ID. When a user changes their password, regenerate the session ID. Each time the trust level of the session changes meaningfully, a new session ID closes any window where an attacker might have had access to the old one.

For a broader view of authentication security patterns, a guide to PHP sessions and login state covers these patterns in a practical context.

Session Hijacking: When the Attacker Steals the Session ID After It Is Set

Session hijacking is when an attacker obtains a valid session ID through some means and uses it to impersonate the user without their knowledge. The most common methods are network eavesdropping, cross-site scripting, and physical device access.

Network eavesdropping is the simplest to understand. If your application is served over HTTP instead of HTTPS, the session cookie travels across the network in plain text. Anyone on the same WiFi network, anyone who can intercept traffic at any point between the user and the server, and anyone with access to the server's network interfaces can read the session cookie and immediately use it. This is why HTTPS is not optional for applications that use sessions. Without it, session hijacking is trivial for anyone on the same network.

Cross-site scripting provides a way to steal cookies even when HTTPS is in use. If the application reflects user input without proper output encoding, an attacker injects a script that reads document.cookie and sends it to a server the attacker controls. The script runs in the context of your domain, which means it has access to cookies that are not marked HttpOnly. This is why output encoding is not optional: XSS and session hijacking are directly connected, and fixing XSS is the primary defence against this attack path.

PHP provides two cookie flags that make session hijacking significantly harder to execute. HttpOnly tells the browser not to make the cookie available to JavaScript. Most modern browsers honour this. It does not prevent all XSS-based cookie theft but it removes the most common script-based path. Secure tells the browser only to send the cookie over HTTPS connections. Even if your site supports both HTTP and HTTPS, the session cookie will only travel over the encrypted connection, which prevents plain-text interception on unencrypted network paths.

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

These flags should be set at the configuration level rather than relying on ini_set calls in application code. The OWASP Top 10 for business web applications covers session-related vulnerabilities alongside other common security risks that affect PHP applications.

Missing Session Timeouts and Abandoned Sessions

PHP sessions last until the browser is closed by default. An authenticated session can persist for hours or days if the browser stays open on a laptop or desktop. This is a serious problem for applications on shared devices. Someone logs in, walks away to a meeting, and leaves a browser open. The next person who sits down has full access to the authenticated session without needing a password.

The fix is to implement session timeout at the application level. Track the last activity timestamp in the session and check on every request whether enough time has passed that the session should be considered expired:

session_start();

$timeout = 1800; // 30 minutes in seconds

if (isset($_SESSION['last_activity']) && 
    (time() - $_SESSION['last_activity'] > $timeout)) {
    session_destroy();
    header('Location: /login?expired=1');
    exit;
}

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

This approach has a practical limitation. The session is only checked when the user makes a request. A session left open on an abandoned browser that never makes another request will not expire until the garbage collector runs, which PHP triggers probabilistically based on session.gc_maxlifetime. For active sessions this approach works well. For abandoned sessions, you also need to set a short session.gc_maxlifetime to ensure the server-side session data is cleaned up within a reasonable window.

The absolute maximum session lifetime should not exceed what the application actually needs. An application where users log in once and stay logged in for weeks needs different handling than a financial application where session expiry after 15 minutes of inactivity is appropriate. Choose the timeout based on what the data in the application warrants.

PHP Session Configuration That You Should Have Enabled Right Now

There are several PHP configuration settings that control session security behaviour. Setting these correctly at the php.ini level, or in an .htaccess or .user.ini file for Apache or in the Nginx PHP-FPM pool configuration, provides a baseline that applies to every session in the application without requiring individual code changes.

session.cookie_httponly = 1
session.cookie_secure = 1
session.use_strict_mode = 1
session.gc_maxlifetime = 1800
session.cookie_samesite = Strict

use_strict_mode is particularly important and it is not enabled by default in most PHP installations. When enabled, PHP will not accept a session ID that the application itself did not create. This prevents attackers from setting a known session ID via URL parameter or cookie and then using it to hijack a session. Without strict mode, PHP accepts any session ID the client sends, even if the application never created that ID. Combined with session fixation, this is a complete attack path. With strict mode enabled, PHP-generated session IDs are the only ones accepted.

SameSite cookies control whether the session cookie is sent with cross-site requests. Setting this to Strict means the cookie is only sent on same-site requests. Setting it to Lax means it is sent on same-site requests and on top-level cross-site GET requests, but not on subresource requests like AJAX or image loads from third-party domains. SameSite: Lax provides significant protection against cross-site request forgery while maintaining better usability for users who navigate to your site from external links. SameSite: Strict provides stronger protection but can create usability issues when links from other sites bring users to your application.

None of these settings are enabled by default in a standard PHP installation. They are not obscure. They do not require additional libraries. They exist in every PHP version that is currently supported. They just need to be enabled.

Session Storage: What You Are Using and What You Should Be Using

PHP stores session data in files by default, in the directory specified by session.save_path. On a shared server, other websites on the same server may be able to read those files if filesystem permissions are not configured correctly. Even on a dedicated server, session files written to a world-readable directory are a problem waiting to happen.

The default /tmp directory on many Linux systems is world-readable. Any user on the server can list /tmp and read session files belonging to other users or other websites. For applications handling sensitive data, session files should be stored in a location that is only accessible to the web server process and the application.

Database session storage solves this and adds the ability to track and invalidate sessions across multiple servers. If your application runs on multiple servers behind a load balancer, file-based sessions cause users to be logged out when their requests hit a different server than the one where their session file exists. A shared database session store keeps sessions consistent across all application instances. More importantly, database storage gives you visibility into active sessions and the ability to revoke them individually or globally, which file storage does not support.

Redis is a better option for session storage in high-traffic applications. It is significantly faster than database storage for read and write operations, supports automatic expiry, and handles concurrent access well. PHP can be configured to use Redis as the session save handler:

session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379?database=0"

For most applications, moving session storage out of the default filesystem location and into a database or Redis is a worthwhile hardening step that takes less than an hour to implement.

What to Check in an Existing Application

Auditing session security in an existing PHP application follows a systematic checklist. Start with configuration and work toward code.

First, check the session cookie flags in the running PHP configuration: session.cookie_httponly, session.cookie_secure, session.use_strict_mode, and session.cookie_samesite. The most common finding in a session security audit is that none of these are enabled. They are straightforward to enable and provide meaningful protection immediately.

Second, check whether session_regenerate_id(true) is called on every successful authentication. Search the codebase for session_regenerate_id and verify it is called with true as the argument immediately after login succeeds. Also check whether it is called at privilege transitions.

Third, check whether the application enforces session timeout at the application level. Look for last_activity or equivalent timestamp tracking in the session. If it is not present, add it. The implementation is a dozen lines of code.

Fourth, check whether session IDs are accepted from URL parameters. If session.use_only_cookies is not set to 1, or if the application explicitly reads session IDs from $_GET or $_REQUEST, that is a session fixation risk. Force cookie-only sessions:

session.use_only_cookies = 1;

Fifth, verify that HTTPS is enforced across the entire application, not just on the login page. A session established on an HTTPS page can be hijacked if the application later serves pages over HTTP and the cookie does not have the Secure flag set.

For a detailed checklist approach to reviewing PHP security, a PHP security checklist for business websites covers session configuration alongside other important hardening steps.