How We Audited a High-Traffic PHP Enterprise Stack on Linode and Mitigated session hijacking through unencrypted session files storage
Initial Assessment: Unencrypted Session Storage Vulnerability
Our engagement began with a critical security audit of a high-traffic PHP enterprise application hosted on Linode. The primary concern, flagged by our preliminary reconnaissance, was the potential for session hijacking due to the application’s default session handling mechanism. Specifically, PHP’s default configuration often writes session data to temporary files on the server’s filesystem. If these files are not adequately protected, or if the server itself is compromised, an attacker could gain access to sensitive session tokens, effectively impersonating legitimate users.
The application in question utilized a standard PHP setup, likely relying on the `session.save_path` directive to dictate where session files were stored. In many default Linode PHP installations, this path is often world-readable or accessible by other users on the same server, especially if not explicitly hardened. This presents a significant attack vector: an attacker gaining shell access to the server, even with limited privileges, could enumerate and read session files belonging to other users.
Identifying the `session.save_path`
The first step was to definitively locate the `session.save_path` being used by the application. This can be achieved through several methods:
- `phpinfo()` output: While not ideal for production environments due to its verbose nature, a temporary `phpinfo()` script can quickly reveal the active configuration.
- `ini_get()` in PHP code: Programmatically querying the setting is safer and more direct.
- Server configuration files: Inspecting `php.ini` and any included `.ini` files.
We opted for a programmatic approach to confirm the setting without exposing unnecessary information.
Programmatic Session Path Discovery
A simple PHP script, executed within the application’s context (e.g., via a temporary file upload or a controlled execution environment), can retrieve this value:
<?php
// Ensure this script is executed in a secure, temporary manner.
// Do NOT leave this in production.
$session_save_path = ini_get('session.save_path');
if ($session_save_path === false || empty($session_save_path)) {
echo "Error: Could not retrieve session.save_path or it is not set.\n";
} else {
echo "Current session.save_path: " . $session_save_path . "\n";
// Further checks: permissions, ownership, etc.
if (is_dir($session_save_path)) {
echo "Path exists.\n";
// Example: Check permissions (octal representation)
$permissions = substr(sprintf('%o', fileperms($session_save_path)), -4);
echo "Permissions: " . $permissions . "\n";
// Example: Check owner and group
$owner_id = fileowner($session_save_path);
$group_id = filegroup($session_save_path);
$owner_info = posix_getpwuid($owner_id);
$group_info = posix_getgrgid($group_id);
echo "Owner: " . ($owner_info['name'] ?? $owner_id) . "\n";
echo "Group: " . ($group_info['name'] ?? $group_id) . "\n";
} else {
echo "Path does not exist or is not a directory.\n";
}
}
?>
On the target Linode instance, the output revealed a common default path like /var/lib/php/sessions. A quick check of its permissions using ls -ld /var/lib/php/sessions often showed something like drwxrwxrwt, indicating world-writable permissions. This is a critical security flaw, as any user on the system could potentially read or even modify session files.
Mitigation Strategy: Encrypted Sessions and Secure Storage
The most robust solution involves two primary components: encrypting session data and storing it in a location inaccessible to other users or processes. We explored several options:
- PHP Session Encryption Libraries: Using libraries that encrypt session data before writing it to disk.
- Database-backed Sessions: Storing sessions in a database (e.g., Redis, Memcached, or a relational database) which offers better control over access and can be configured for encryption.
- Custom File Encryption: Implementing custom logic to encrypt/decrypt session data before serialization/deserialization.
- Securing the Default Path: While less ideal, restricting access to the default file path.
For this enterprise-grade application, we recommended a combination of a well-vetted PHP session encryption library and a secure, dedicated storage location. This provides a layered defense.
Implementing Session Encryption with `php-session-encrypt`
We chose the php-session-encrypt library for its straightforward integration and strong encryption capabilities. It leverages OpenSSL for robust AES-256-CBC encryption.
First, ensure the library is installed via Composer:
composer require moxie/php-session-encrypt
Next, we need to configure PHP to use this handler. This involves modifying the `php.ini` file or using a custom handler script.
Modifying `php.ini` for Custom Session Handler
Locate your active `php.ini` file. This is typically found in /etc/php/[version]/cli/php.ini and /etc/php/[version]/fpm/php.ini (for FPM/web server usage). We need to set the following directives:
; Ensure session.auto_start is Off unless you have a specific reason session.auto_start = Off ; Define a secure, dedicated directory for session files ; This directory MUST be created and have restrictive permissions. session.save_path = "/var/lib/app_sessions" ; Use the custom session save handler session.save_handler = files ; Enable strict mode for session IDs to prevent fixation session.use_strict_mode = 1 ; Disable transparent sid re-writing (use cookies only) session.use_trans_sid = 0 ; Set a reasonable cookie lifetime (e.g., 30 minutes) session.cookie_lifetime = 1800 ; Set a reasonable garbage collection probability and lifetime session.gc_probability = 1 session.gc_divisor = 100 session.gc_maxlifetime = 1800 ; Match cookie lifetime or slightly longer ; --- Custom Encryption Settings --- ; These are NOT standard PHP directives. They are used by the custom handler. ; You'll need to define these in your application's configuration. ; For php-session-encrypt, we'll use a custom handler script.
The key here is `session.save_path`. We’ve changed it to a dedicated directory, /var/lib/app_sessions. This directory must be created and secured.
Creating and Securing the Session Directory
On the Linode server, execute the following commands:
# Create the directory
sudo mkdir -p /var/lib/app_sessions
# Set ownership to the web server user (e.g., www-data for Apache/Nginx with PHP-FPM)
# Find your web server user: ps aux | egrep '(apache|httpd|nginx)' | grep -v root | head -n1 | awk '{print $1}'
sudo chown www-data:www-data /var/lib/app_sessions
# Set restrictive permissions: only owner can read/write/execute
sudo chmod 700 /var/lib/app_sessions
This ensures that only the web server process can access the session files, significantly reducing the risk of unauthorized access even if other users gain shell access.
Integrating the Encryption Handler
The php-session-encrypt library provides a custom session handler. We need to register it. A common place to do this is in your application’s bootstrap or a dedicated configuration file that’s included early in the request lifecycle.
First, generate an encryption key. This should be a strong, random key stored securely and not hardcoded directly in version control if possible. For this example, we’ll assume it’s loaded from environment variables or a secure configuration file.
# Example of generating a key (run this once, store securely) openssl rand -base64 32
Then, in your PHP application’s entry point (e.g., index.php or a bootstrap file):
<?php
require 'vendor/autoload.php'; // Composer autoloader
use Moxie\Session\EncryptSessionHandler;
// Load your encryption key securely (e.g., from environment variables)
$encryptionKey = getenv('APP_SESSION_ENCRYPTION_KEY'); // e.g., 'your_super_secret_32_byte_key_here'
if (!$encryptionKey) {
die("Error: APP_SESSION_ENCRYPTION_KEY is not set.\n");
}
// Instantiate the custom session handler
$handler = new EncryptSessionHandler($encryptionKey);
// Register the custom handler
session_set_save_handler($handler, true); // The 'true' argument enables automatic session start if needed
// Start the session
session_start();
// Now you can use $_SESSION as usual
$_SESSION['user_id'] = 123;
$_SESSION['last_login'] = time();
// ... rest of your application logic
?>
With this setup, PHP will now use the EncryptSessionHandler. When session_start() is called, the handler will:
- Attempt to load the session data from the file specified by
session.save_path. - If data exists, it will be decrypted using the provided key.
- If no data exists or it’s invalid, a new session will be initialized.
- When the script finishes execution (or when
session_write_close()is called), the session data will be serialized, encrypted, and written back to the file.
Alternative: Database-Backed Sessions (Redis Example)
For very high-traffic applications or environments where filesystem I/O is a bottleneck, using an in-memory data store like Redis is a superior alternative. Redis can be configured for persistence and offers excellent performance.
First, install the Redis extension for PHP:
sudo apt-get update sudo apt-get install php-redis sudo systemctl restart php[version]-fpm
Then, configure php.ini:
session.save_handler = redis session.save_path = "tcp://127.0.0.1:6379?auth=your_redis_password" ; If Redis requires authentication ; Other session settings as before (use_strict_mode, etc.) session.use_strict_mode = 1 session.use_trans_sid = 0 session.cookie_lifetime = 1800 session.gc_maxlifetime = 1800
If Redis is running on a different server or requires specific configuration, adjust the `session.save_path` accordingly. For enhanced security, Redis can be configured to use TLS/SSL for encrypted communication between the PHP process and the Redis server.
Post-Mitigation Verification and Monitoring
After implementing the chosen mitigation strategy, a thorough verification process is crucial:
- Functional Testing: Ensure all application features relying on sessions (login, user state, shopping carts, etc.) function correctly.
- Security Auditing: Re-run the initial checks. Verify that session files (if still used) are unreadable by other users, or that session data in Redis is inaccessible without proper authentication.
- Log Analysis: Monitor PHP error logs and web server logs for any new session-related errors.
- Performance Monitoring: Track application response times and resource utilization (CPU, memory, I/O) to ensure the new session handling mechanism doesn’t introduce performance regressions.
For ongoing security, we recommend implementing regular automated security audits and maintaining strict access controls on the Linode server. Regularly review PHP configurations and ensure all dependencies, including session handling libraries, are kept up-to-date.
Conclusion
The default PHP session handling, while convenient, poses a significant security risk in multi-user environments like shared hosting or even dedicated servers if not properly secured. By implementing encrypted session storage, either through custom handlers or robust external stores like Redis, and by ensuring strict filesystem permissions, we can effectively mitigate the threat of session hijacking. This case study highlights the importance of a proactive security posture, where common configurations are scrutinized and hardened to protect sensitive user data and maintain application integrity.