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. Reviewing your server's overall security posture through a structured hardening process can catch configuration gaps that affect session cookies.
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.
It is worth noting that user agent fingerprinting is a basic check rather than a foolproof method. Determined attackers can spoof user agent headers. Treat this as one layer among several rather than a complete solution.
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');
Using Redis for session storage also improves performance under high traffic, since Redis keeps session data in memory rather than reading from disk on each request.
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.
Sessions and AJAX Requests
Modern web applications often use AJAX requests alongside traditional page loads. Sessions work the same way for AJAX requests, but there are a few practical considerations.
Session expiry checks should run on every AJAX request, not just page loads. An idle user who keeps a page open in a background tab may still trigger AJAX requests that reset the idle timer. This can keep sessions alive indefinitely when they should have expired. To handle this correctly, either skip updating last_activity for AJAX requests or use a separate, shorter idle timeout specifically for AJAX activity.
When returning session-related errors from AJAX endpoints, return a proper HTTP status code rather than relying on the response body. A 401 Unauthorized response allows client-side JavaScript to detect the expired session and redirect the user to the login page programmatically.
if (isset($_SESSION['last_activity']) && (time() - $_SESSION['last_activity'] > 900)) {
http_response_code(401);
echo json_encode(['error' => 'Session expired']);
exit;
}
CSRF token validation should apply to AJAX requests that modify state, just as it applies to form submissions. Include the CSRF token in a request header rather than the request body for cleaner code.
fetch('/api/update-profile', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': ''
},
body: JSON.stringify({ email: 'new@example.com' })
});
Database-Backed Session Storage
Using the filesystem for session storage is simple but has limitations in scaled environments. When your application runs on multiple servers, each server has its own filesystem, meaning a user might hit a different server on each request and lose their session data.
Storing sessions in a database resolves this by providing a shared session store accessible from any server. A basic sessions table is straightforward to set up.
CREATE TABLE sessions (
id VARCHAR(128) PRIMARY KEY,
user_id INT NULL,
data TEXT,
last_activity INT,
created_at INT,
INDEX idx_last_activity (last_activity)
);
Set the session save handler to use your custom implementation or a library that handles the database operations. When using database-backed sessions, clean up old records regularly to prevent the table from growing indefinitely. A cron job that removes sessions older than your absolute timeout period is usually sufficient.
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
SIDin 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.
- Not validating session data on retrieval: Always check that session variables contain expected types and values before using them. An attacker who finds a way to write to the session could inject unexpected data.
- Skipping logout on the server side: Relying entirely on client-side cookie removal means a server-side session file can still be valid. Always call
session_destroy()on the server.
Reviewing your application for these issues is worth doing periodically, particularly when adding new features that affect authentication or user state.
Monitoring Session Activity
Beyond implementing session security controls, monitoring session activity helps identify suspicious behaviour before it becomes a problem. Track metrics such as multiple sessions from different IP addresses for the same user, sessions created in quick succession from different user agents, and failed login attempts followed by successful logins from different locations.
These patterns do not automatically indicate an attack. Legitimate users switch between devices, use VPNs that change their IP address, or access accounts from work and home. However, they warrant investigation and may indicate credential compromise.
Logging session-related events at a reasonable level of detail supports incident response. Log session creation, regeneration, and destruction events along with relevant request metadata. Avoid logging sensitive session data itself, since logs often end up in locations with weaker access controls.
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.
Related practical reading
These related guides can help you connect this topic with the wider website, server, security, and support decisions around it.
- SSH Config Tips That Save Hours of Time - useful background for related technology decisions
- How to Build a PHP Webhook Receiver: Complete Implementation Guide - useful background for related technology decisions
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.