The files handler ties each visitor to one server. Behind a load balancer, use Redis 2,763 via phpredis 10,226 (github.com/phpredis/phpredis (https://github.com/phpredis/phpredis 10,226 ); sudo apt install php8.5-redis, then session.save_handler = redis), Memcached 4,600 , or a class implementing SessionHandlerInterface. Extending the built-in SessionHandler changes only what you override; this one encrypts the files at rest with libsodium (Encryption):
<?php
declare(strict_types=1);
final class SealedSessionHandler extends SessionHandler // the files handler, encrypted
implements SessionUpdateTimestampHandlerInterface
{
public function __construct(private string $key) {} // 32 bytes from the environment
public function read(string $id): string|false {
$box = parent::read($id);
if (!$box) return $box; // '' for a new session
$plain = sodium_crypto_secretbox_open(substr($box, 24), substr($box, 0, 24), $this->key);
return $plain === false ? '' : $plain; // tampered: start empty
}
public function write(string $id, string $data): bool {
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES); // 24 bytes
return parent::write($id, $nonce . sodium_crypto_secretbox($data, $nonce, $this->key));
}
public function validateId(string $id): bool { // restores use_strict_mode
return is_file(session_save_path() . "/sess_$id");
}
public function updateTimestamp(string $id, string $data): bool { // lazy_write
return touch(session_save_path() . "/sess_$id");
}
}sealed.php registers it with session_set_save_handler() and includes cart.php:
$ curl -s -c s.txt '127.0.0.1:8215/sealed.php?add=BK-101'
BK-101 x 1
$ sudo base64 -w 72 /var/lib/php/sessions/sess_$(awk '$6 == "PHPSESSID" {print $7}' s.txt)
uvWk023MkVwNL0WbUjZNNXmPLNDmGfCI+4GLgWzHKvb+mJn+wLu6y4BHBpzVVJPbIMBo3ZIu
gTY0EajVrZJLgldINSA=
$ curl -si -b PHPSESSID=forged00000000000000000000000001 127.0.0.1:8215/sealed.php \
| grep -i ^set-cookie | fold_cookies
Set-Cookie: PHPSESSID=2cbb9d30b59473c63c7b9ebaea2336e7;
path=/; secure; HttpOnly; SameSite=LaxThe forged ID was replaced only thanks to validateId(); without it, this class accepted forged…1, silently disabling strict mode. Handlers must also lock. The files handler holds an flock() until the script ends, so one visitor's requests run in turn: a page holding its session for two seconds delayed a second request by 1.8 s here, and session_write_close() before the slow work cut that to 1 ms. phpredis locks only with redis.session.locking_enabled = 1.