In short: Four things count for PHP session security: secure cookie settings, a new session ID after every login, separate and protected storage locations, and short lifetimes. Because of PHP2Shell, session.upload_progress.enabled=0 should also be set on the server side, as long as there is no reason for this feature.
Why sessions are a worthwhile target
A session remembers that a user is logged in. Technically, the browser receives a random session ID in a cookie for this, and the server stores the associated data. Whoever knows this ID is the same user as far as the application is concerned, without a password and without a second factor.
For operators this means: weak session handling can undermine login protection even if passwords and two-factor login are implemented cleanly. This is particularly critical for admin areas, customer accounts and portals with personal data.
The typical risks are the theft of the session ID (session hijacking), planting a known ID before login (session fixation) and sessions that stay valid too long on shared computers.
Secure basic settings in php.ini
The most important values can be set centrally in php.ini or per website in the FPM pool. In the pool, use php_admin_value or php_admin_flag so that the application cannot override the values.
; Basic session hardening
session.use_strict_mode = 1
session.use_only_cookies = 1
session.use_trans_sid = 0
session.cookie_httponly = 1
session.cookie_secure = 1
session.cookie_samesite = "Lax"
session.cookie_lifetime = 0
session.gc_maxlifetime = 1800
session.name = "APPSESSID"
; PHP2Shell note from php.net, then restart all workers
session.upload_progress.enabled = 0
use_strict_mode ensures that PHP does not accept unknown IDs supplied by the browser. cookie_httponly prevents JavaScript from reading the ID, which limits the consequences of cross-site scripting. cookie_secure sends the cookie only over HTTPS. SameSite=Lax makes cross-site requests with a valid session harder; for pure admin areas, Strict may be suitable.
The lifetime of 1,800 seconds is an example value. Set it according to the protection needed: shorter for administration and customer data, longer only where it is functionally necessary.
Note that gc_maxlifetime only determines from when old session data may be deleted. On some distributions it is not PHP itself but a regular system job that cleans up the session files. Check which value that job uses so that your setting actually takes effect. Also, for logging out after inactivity, do not rely on the cleanup function alone; check the time of the last action in the application itself, as shown in the following example.
Renewing the session ID after login
The most important rule in the code is to regenerate the session ID at every change of privilege, that is, at login, when switching to an admin mode and at logout. The following example shows a clean basic pattern with password_verify():
<?php
declare(strict_types=1);
session_start();
function anmelden(PDO $db, string $email, string $passwort): bool
{
$stmt = $db->prepare('SELECT id, passwort_hash FROM benutzer WHERE email = :email');
$stmt->execute(['email' => $email]);
$nutzer = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$nutzer || !password_verify($passwort, $nutzer['passwort_hash'])) {
return false; // same message for a wrong address and a wrong password
}
session_regenerate_id(true); // neue ID, alte verwerfen
$_SESSION['benutzer_id'] = (int) $nutzer['id'];
$_SESSION['letzte_aktion'] = time();
return true;
}
function pruefeLeerlauf(int $sekunden = 900): void
{
if (isset($_SESSION['letzte_aktion']) && time() - $_SESSION['letzte_aktion'] > $sekunden) {
abmelden();
return;
}
$_SESSION['letzte_aktion'] = time();
}
function abmelden(): void
{
$_SESSION = [];
$p = session_get_cookie_params();
setcookie(session_name(), '', [
'expires' => time() - 3600,
'path' => $p['path'],
'domain' => $p['domain'],
'secure' => $p['secure'],
'httponly' => $p['httponly'],
'samesite' => $p['samesite'],
]);
session_destroy();
}
In addition, protect forms that change something with a CSRF token. It is generated with bin2hex(random_bytes(32)), stored in the session and compared with hash_equals() on submission.
Storage locations and tenant separation
By default, PHP stores sessions as files in a shared directory. On a server with several websites, this can mean that a compromised website can read another's sessions. That is why every website needs its own pool with its own user and its own session path.
; in the FPM pool of the respective website
php_admin_value[session.save_path] = /var/lib/php/sessions/kunde-a
The directory should belong to the pool user and have the permissions 0700. How to additionally seal websites off from each other is described in the guide open_basedir and isolation per website. If sessions are stored in Redis or a database, the same principles apply: separate access per application, no open network access.
PHP session security and PHP2Shell
The PHP2Shell vulnerabilities CVE-2026-109618 and CVE-2026-109619 lie in PHP itself. As one of the interim measures, php.net names setting session.upload_progress.enabled=0 in php.ini, in the FPM pool or in the server configuration and then restarting all workers. Explicitly not via ini_set() or a .user.ini.
On 23 October 2026 a note on CVE-2026-115560 followed. Possible code execution exists there only with an unusual combination of session serialisation, a switch via ini_set() before session_start() and active upload progress. The workaround is the same setting, until the next regular bugfix release.
This setting is an interim measure and not complete protection. What remains decisive is the update to a fixed PHP version and the subsequent restart of all PHP workers.
After the change, check which value actually applies. The command line often shows a different configuration from the web server; what is conclusive is a protected check page that is deleted again afterwards, or the pool configuration itself.
Typical mistakes with sessions in PHP applications
Many vulnerabilities arise not from missing settings but from the way applications use sessions. You should specifically ask about these patterns in your own code and with service providers:
- Too much in the session: Passwords, complete payment data or large data sets do not belong in the session. Store only what is needed for assignment, such as the user ID and roles.
- Permissions only from the session: Whether someone is admin should be checked against the database for critical actions. Otherwise revoked permissions stay effective until the session expires.
- Session ID in the address: IDs in URLs end up in logs, browser histories and referrer information.
use_only_cookiesanduse_trans_sid = 0prevent that. - No end to the session: Without an idle timeout, an admin on a shared computer stays logged in indefinitely.
- HTTPS wrongly detected behind a proxy: If encryption ends at the load balancer, the application must know. Otherwise it may set cookies without the
Secureflag.
After a security incident, it also belongs to discard all existing sessions. The simplest way is to delete the contents of the session directory or change the session name. All users then have to log in again, and stolen sessions become worthless.
How does PHP SOS help?
- We check session settings, cookie flags and storage locations on all websites of a server and document the values that are actually effective.
- We apply the PHP2Shell interim measures per pool, restart the workers in a controlled way and provide proof of the result.
- We separate websites with their own pools and session paths as part of server hardening.
- For custom developments, we check login, logout and session changes in the code and recommend concrete changes.
Frequently asked questions
What is session hijacking?
In session hijacking, an attacker takes over the session of a logged-in user by obtaining their session ID. With the ID, they are that user as far as the application is concerned, without knowing the password. Secure cookie flags and short lifetimes make this considerably harder.
Why must the session ID be regenerated after login?
If the ID stays the same before and after login, an ID known beforehand or planted could remain valid after logging in. session_regenerate_id(true) creates a new ID and discards the old one.
Can I disable session.upload_progress.enabled via ini_set()?
No, according to php.net that is not sufficient. The setting belongs in php.ini, the FPM pool or the server configuration, not in ini_set() or a .user.ini. Afterwards all PHP workers must be restarted.
Where should session files be stored?
In a directory outside the web root that only the user of the respective PHP pool may read. On servers with several websites, each website should have its own session directory.