• Skip to secondary menu
  • Skip to main content
  • Skip to primary sidebar
  • Home
  • Projects
  • Products
  • Themes
  • Tools
  • Request for Quote

Vengala Vinay

Having 9+ Years of Experience in Software Development

  • Home
  • WordPress
  • PHP
    • Codeigniter
  • Django
  • Magento
  • Selenium
  • Server
Home » How We Audited a High-Traffic PHP Enterprise Stack on OVH and Mitigated session hijacking through unencrypted session files storage

How We Audited a High-Traffic PHP Enterprise Stack on OVH and Mitigated session hijacking through unencrypted session files storage

Initial Assessment: Unencrypted Session Files on OVH

During a routine security audit of a high-traffic enterprise PHP application hosted on OVH’s infrastructure, we identified a critical vulnerability: PHP session files were being stored unencrypted on the filesystem. This configuration, while common in development or low-security environments, poses a significant risk in production, especially when sensitive user data is involved. The default PHP session handler (`files`) writes session data to files, typically located in a directory specified by the `session.save_path` directive in `php.ini`. On OVH’s shared or even dedicated server environments, if this path is not properly secured or if the underlying filesystem is compromised, an attacker could potentially read, modify, or steal session cookies, leading to session hijacking.

The application in question handled user authentication, financial transactions, and proprietary data, making session hijacking a high-impact threat. The OVH environment, while robust, relies on proper server configuration to maintain security. Our initial investigation focused on locating the `session.save_path` and verifying its accessibility.

Locating and Inspecting `session.save_path`

The first step was to determine the active `session.save_path` for the running PHP processes. This can be achieved by creating a simple PHP script that outputs the relevant configuration directives. We deployed a temporary script named `phpinfo.php` to a publicly accessible (but soon-to-be-removed) directory on one of the web servers.

Note: This script should be removed immediately after use due to the sensitive information it exposes.

<?php
phpinfo();
?>

Accessing this script via a web browser provided a detailed output of the PHP environment. We specifically looked for the following directives:

  • session.save_handler: Confirmed it was set to ‘files’.
  • session.save_path: This is the critical path we needed to identify. It often defaults to `/var/lib/php/sessions` or a similar system-defined directory, but can be overridden in `php.ini`, `.htaccess`, or via `ini_set()` in the application code.

On the OVH servers, we found that `session.save_path` was indeed pointing to a directory like `/var/lib/php/sessions`. We then used SSH to access the server and inspect the permissions and ownership of this directory.

ssh user@your_server_ip
ls -ld /var/lib/php/sessions

The output typically showed something like:

drwxrwx--- 2 www-data www-data 4096 Jan 1 10:00 /var/lib/php/sessions

This indicated that the web server user (e.g., `www-data`) and its group had read and write permissions. While this is necessary for PHP to function, it means any process running as `www-data` or any user with elevated privileges could access these files. In a shared hosting environment, or if a web application vulnerability allowed privilege escalation, these session files would be exposed.

Mitigation Strategy 1: Encrypting Session Data

The most direct way to mitigate the risk of unencrypted session files is to encrypt the data before it’s written to disk and decrypt it upon retrieval. PHP doesn’t have built-in transparent encryption for the file session handler. However, we can implement this by creating a custom session handler.

A custom session handler in PHP must implement the `SessionHandlerInterface`. This interface defines six methods: `open()`, `close()`, `read()`, `write()`, `destroy()`, and `gc()`. We will focus on `read()` and `write()` for encryption/decryption.

Implementing a Custom Encrypted Session Handler

We developed a custom handler that leverages PHP’s OpenSSL extension for robust encryption. The handler requires a secret key, which must be securely stored and managed. For this example, we’ll assume the key is loaded from environment variables or a secure configuration file.

// Define a secure encryption key (e.g., from environment variable)
define('SESSION_ENCRYPTION_KEY', getenv('APP_SESSION_KEY') ?: 'a_very_strong_and_secret_key_that_is_at_least_32_bytes_long');
define('SESSION_ENCRYPTION_IV_LENGTH', openssl_cipher_iv_length('aes-256-cbc'));

class EncryptedSessionHandler implements SessionHandlerInterface {
    private $savePath;
    private $key;
    private $ivLength;

    public function __construct(string $savePath = '/var/lib/php/sessions') {
        $this->savePath = $savePath;
        $this->key = SESSION_ENCRYPTION_KEY;
        $this->ivLength = SESSION_ENCRYPTION_IV_LENGTH;

        if (!is_dir($this->savePath)) {
            if (!mkdir($this->savePath, 0700, true)) {
                throw new \RuntimeException("Failed to create session save path: {$this->savePath}");
            }
        }
    }

    public function open(string $savePath, string $sessionName): bool {
        $this->savePath = $savePath;
        return true;
    }

    public function close(): bool {
        return true;
    }

    public function read(string $id): string {
        $filePath = $this->savePath . '/sess_' . $id;
        if (!file_exists($filePath) || !is_readable($filePath)) {
            return '';
        }

        $encryptedData = file_get_contents($filePath);
        if ($encryptedData === false) {
            return '';
        }

        // Extract IV and ciphertext
        $ivLength = $this->ivLength;
        if (strlen($encryptedData) < $ivLength) {
            // Data too short to contain IV
            return '';
        }

        $iv = substr($encryptedData, 0, $ivLength);
        $ciphertext = substr($encryptedData, $ivLength);

        // Decrypt
        $decrypted = openssl_decrypt($ciphertext, 'aes-256-cbc', $this->key, OPENSSL_RAW_DATA, $iv);

        if ($decrypted === false) {
            // Decryption failed, possibly due to wrong key or corrupted data
            error_log("Session decryption failed for ID: " . $id);
            return '';
        }

        return $decrypted;
    }

    public function write(string $id, string $data): bool {
        $filePath = $this->savePath . '/sess_' . $id;

        // Generate a new IV for each write
        $iv = openssl_random_pseudo_bytes($this->ivLength);
        if ($iv === false) {
            error_log("Failed to generate IV for session ID: " . $id);
            return false;
        }

        // Encrypt
        $ciphertext = openssl_encrypt($data, 'aes-256-cbc', $this->key, OPENSSL_RAW_DATA, $iv);
        if ($ciphertext === false) {
            error_log("Session encryption failed for ID: " . $id);
            return false;
        }

        // Prepend IV to ciphertext
        $encryptedData = $iv . $ciphertext;

        // Write to file with restricted permissions
        if (file_put_contents($filePath, $encryptedData, LOCK_EX) === false) {
            error_log("Failed to write session data to file: " . $filePath);
            return false;
        }

        // Ensure file has restrictive permissions
        if (!chmod($filePath, 0600)) {
            error_log("Failed to set restrictive permissions on session file: " . $filePath);
            // Continue, but log the issue
        }

        return true;
    }

    public function destroy(string $id): bool {
        $filePath = $this->savePath . '/sess_' . $id;
        if (file_exists($filePath)) {
            if (!unlink($filePath)) {
                error_log("Failed to delete session file: " . $filePath);
                return false;
            }
        }
        return true;
    }

    public function gc(int $maxlifetime): int {
        $files = glob($this->savePath . '/sess_*');
        if ($files === false) {
            return 0;
        }

        $deletedCount = 0;
        $now = time();
        foreach ($files as $file) {
            if (is_file($file) && (filemtime($file) + $maxlifetime < $now)) {
                if (unlink($file)) {
                    $deletedCount++;
                }
            }
        }
        return $deletedCount;
    }
}

// Register the custom handler
$sessionSavePath = ini_get('session.save_path');
$encryptedHandler = new EncryptedSessionHandler($sessionSavePath);
session_set_save_handler($encryptedHandler, true);

// Start the session
session_start();

// Example usage:
// $_SESSION['user_id'] = 123;
// echo $_SESSION['user_id'];
?>

Key Considerations for this implementation:

  • Encryption Key Management: The `SESSION_ENCRYPTION_KEY` must be strong, unique, and kept secret. Storing it directly in code is a bad practice. Use environment variables, a secrets management system (like HashiCorp Vault, AWS Secrets Manager, or OVH’s own solutions if available), or a secure configuration file with restricted access.
  • IV Handling: A unique Initialization Vector (IV) is generated for each encryption operation. The IV is prepended to the ciphertext and stored. This is crucial for security, as it ensures that encrypting the same plaintext multiple times results in different ciphertexts.
  • Error Handling: Robust error logging is essential for debugging decryption failures or file write issues.
  • File Permissions: The `chmod($filePath, 0600)` ensures that only the owner of the file (typically the web server user) can read or write to it.
  • Garbage Collection: The `gc()` method is implemented to clean up old session files, similar to the default handler.

Configuring PHP to Use the Custom Handler

To make PHP use this custom handler, you need to register it. This can be done early in your application’s bootstrap process, before `session_start()` is called. The `session_set_save_handler()` function is used for this purpose.

If you have access to `php.ini` or can use `.htaccess` with `php_value` (depending on your OVH hosting plan and PHP configuration), you can set `session.save_handler` and `session.save_path`. However, registering the handler in code is often more flexible and less dependent on server configuration.

Mitigation Strategy 2: Securing the Session Save Path

If encrypting session data is not feasible due to performance concerns or complexity, the next best approach is to harden the security of the existing `session.save_path`. This involves ensuring that the directory is inaccessible to unauthorized users and processes.

Restricting Access to `session.save_path`

Assuming the `session.save_path` is `/var/lib/php/sessions`, we can apply stricter permissions and potentially use Access Control Lists (ACLs) for finer-grained control.

# Ensure the directory is owned by the web server user/group
sudo chown www-data:www-data /var/lib/php/sessions

# Set restrictive permissions: only owner can read/write/execute
sudo chmod 700 /var/lib/php/sessions

# Verify permissions
ls -ld /var/lib/php/sessions

The output should now reflect:

drwx------ 2 www-data www-data 4096 Jan 1 10:00 /var/lib/php/sessions

This ensures that only the `www-data` user can access the session files. However, this doesn’t protect against vulnerabilities within the web server process itself that could be exploited to read files owned by `www-data`.

Moving `session.save_path` to a Non-Web-Accessible Location

A crucial step is to ensure the `session.save_path` is outside the web server’s document root. If your web root is, for example, `/var/www/html`, you must never set `session.save_path` to a subdirectory within it. The default `/var/lib/php/sessions` is usually a safe bet, but it’s always worth verifying.

If you need to change it, you can do so in `php.ini` or via `ini_set()` in your application’s bootstrap file:

<?php
// Ensure this is called before session_start()
$newSessionPath = '/var/sessions'; // A directory outside the web root

// Create the directory if it doesn't exist and set permissions
if (!is_dir($newSessionPath)) {
    if (!mkdir($newSessionPath, 0700, true)) {
        // Handle error: cannot create session directory
        die("Error: Could not create session directory.");
    }
} else {
    // Ensure correct permissions if it already exists
    chmod($newSessionPath, 0700);
}

ini_set('session.save_path', $newSessionPath);
session_start();
?>

This configuration prevents an attacker from directly accessing session files even if they manage to exploit a vulnerability that allows them to view files within the web root.

Mitigation Strategy 3: Using Database or Cache for Sessions

For high-traffic applications, relying on filesystem sessions can become a bottleneck. Furthermore, it introduces the security concerns we’ve discussed. A more robust and often more performant solution is to store sessions in a database or a distributed cache like Redis or Memcached.

Database Sessions

Many frameworks provide built-in support for database sessions. If not, you can implement a custom session handler that interacts with your database (e.g., MySQL, PostgreSQL). This approach centralizes session data and leverages the database’s security and access control mechanisms.

A typical database schema for sessions might look like this:

CREATE TABLE sessions (
    session_id VARCHAR(255) PRIMARY KEY,
    session_data TEXT NOT NULL,
    expires BIGINT UNSIGNED NOT NULL
);

-- Add an index for faster lookups and garbage collection
CREATE INDEX idx_sessions_expires ON sessions (expires);

You would then write a custom session handler that performs `INSERT`, `UPDATE`, `SELECT`, and `DELETE` operations on this table. The `session_data` column would store the serialized session data. Encryption can be applied here as well, either at the application level before storing in the database or by using database-level encryption features.

Redis/Memcached Sessions

Using Redis or Memcached for sessions offers significant performance benefits due to their in-memory nature. Most modern PHP frameworks have excellent support for these. You would configure PHP to use the Redis or Memcached session handler.

For Redis, you’d typically set these in `php.ini` or via `ini_set()`:

session.save_handler = redis
session.save_path = "tcp://127.0.0.1:6379?auth=your_redis_password" ; Or "unix:///var/run/redis/redis.sock"

For Memcached:

session.save_handler = memcache
session.save_path = "tcp://127.0.0.1:11211" ; Or multiple servers: "tcp://host1:port,tcp://host2:port"

Security Note: Ensure your Redis/Memcached instances are properly secured, especially if they are accessible over the network. Use strong passwords (for Redis), bind them to localhost, and consider TLS/SSL encryption if they are not on a trusted private network.

Post-Mitigation Verification and Ongoing Monitoring

After implementing any of the above mitigation strategies, it is crucial to verify their effectiveness and establish ongoing monitoring.

Verification Steps

  • Re-inspect `phpinfo()`: Confirm that `session.save_handler` is set to your custom handler or the desired database/cache handler, and `session.save_path` is correctly configured.
  • Test Session Functionality: Log in, perform actions that modify session data, log out, and verify that sessions are created, updated, and destroyed correctly.
  • Filesystem Check (if applicable): If using a custom file handler, verify that session files are being created in the new path with the correct, restrictive permissions. If using encryption, attempt to read the raw session files to ensure they are unreadable without the key.
  • Penetration Testing: Conduct targeted penetration tests to attempt session hijacking or unauthorized access to session data.

Ongoing Monitoring

  • Log Analysis: Monitor web server logs, PHP error logs, and application logs for any unusual activity related to session handling, decryption failures, or unauthorized access attempts.
  • System Audits: Regularly audit server configurations, file permissions, and user access to ensure no new vulnerabilities are introduced.
  • Security Scans: Employ automated security scanning tools to detect potential misconfigurations or known vulnerabilities.
  • Alerting: Set up alerts for critical security events, such as repeated failed login attempts, unauthorized file access, or high rates of session errors.

By addressing the unencrypted session file storage vulnerability and implementing robust session management practices, we significantly enhanced the security posture of the enterprise PHP application on OVH, protecting sensitive user data from potential session hijacking attacks.

Primary Sidebar

A little about the Author

Having 9+ Years of Experience in Software Development.
Expertised in Php Development, WordPress Custom Theme Development (From scratch using underscores or Genesis Framework or using any blank theme or Premium Theme), Custom Plugin Development. Hands on Experience on 3rd Party Php Extension like Chilkat, nSoftware.

Recent Posts

  • Disaster Recovery 101: Architecting Auto-Failovers for Redis and PHP Deployments on OVH
  • How We Audited a High-Traffic WooCommerce Enterprise Stack on Google Cloud and Mitigated Race conditions during high-concurrency payment processing
  • Disaster Recovery 101: Architecting Auto-Failovers for Elasticsearch and Magento 2 Deployments on DigitalOcean
  • An Auditor’s Checklist for Securing WordPress Backends on OVH
  • Step-by-Step: Diagnosing Perl script high CPU throttling due to unoptimized regular expressions on AWS Servers

Copyright © 2026 · Vinay Vengala