• 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 DigitalOcean and Mitigated session hijacking through unencrypted session files storage

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

Initial Stack Assessment and Discovery

Our engagement began with a deep dive into a high-traffic PHP enterprise application hosted on DigitalOcean. The primary objective was to identify and remediate potential security vulnerabilities, with a specific focus on session management. The stack comprised a typical LAMP (Linux, Apache, MySQL, PHP) configuration, with PHP-FPM handling application requests, Apache serving static assets, and MySQL as the primary data store. The application experienced significant user load, making session hijacking a critical threat vector.

The initial audit involved several key steps:

  • Infrastructure Review: We examined the DigitalOcean Droplet configurations, network security groups (firewalls), and load balancer (if any) settings.
  • Application Code Audit: A targeted review of the PHP codebase, focusing on session handling, authentication, authorization, and input validation.
  • Configuration Analysis: Scrutiny of PHP, Apache, and MySQL configuration files for insecure defaults or misconfigurations.
  • Traffic Monitoring: Utilizing tools like tcpdump and Wireshark to analyze network traffic patterns and identify potential data leakage.

During the infrastructure review, we noted that session files were being stored on the local filesystem of the web servers. While common, this approach presents a significant risk if the filesystem is not adequately secured or if sensitive data is transmitted unencrypted over the network.

Unencrypted Session File Storage: The Vulnerability

PHP’s default session handler, `files`, stores session data in serialized form within files. The default location is often within the web server’s document root or a temporary directory. The critical vulnerability arises when these session files are accessible by unauthorized processes or when the data within them is transmitted insecurely.

Consider a scenario where a web server is compromised. An attacker gaining filesystem access could potentially read session files, extract session IDs, and impersonate legitimate users. Furthermore, if the application or server infrastructure involves any form of inter-server communication (e.g., for session replication or distributed caching) that is not encrypted, session IDs could be intercepted in transit.

The PHP configuration directive `session.save_path` dictates where these files are stored. A common, albeit insecure, default might be something like `/var/lib/php/sessions` or even a subdirectory within the web root. If this directory is not properly permissioned, or if the underlying storage is shared and accessible by other services, the risk escalates.

Auditing Session File Permissions and Access

Our first step in auditing was to verify the `session.save_path` and its associated permissions. This is typically found in `php.ini` or can be dynamically checked within a PHP script.

1. Locating `php.ini`:

On a typical DigitalOcean Droplet running PHP-FPM, you might find multiple `php.ini` files. The one relevant to PHP-FPM is usually located at `/etc/php/[version]/fpm/php.ini`. You can confirm this by creating a simple PHP file:

`info.php`

<?php
phpinfo();
?>

Accessing this file via a web browser will display detailed PHP configuration, including the loaded `php.ini` file and the `session.save_path` directive.

2. Checking `session.save_path` and Permissions:

Once the path is identified (e.g., `/var/lib/php/sessions`), we use standard Linux commands to inspect its ownership and permissions.

sudo ls -ld /var/lib/php/sessions

The output should reveal the owner, group, and permissions. For instance:

drwx-wx-wt 2 www-data www-data 4096 Oct 26 10:00 /var/lib/php/sessions

In this example, `www-data` is the user and group that PHP-FPM runs as. The permissions `drwx-wx-wt` indicate that the owner (`www-data`) has read, write, and execute permissions. The sticky bit (`t`) at the end is crucial for directories where multiple users might create files; it prevents users from deleting or renaming files they don’t own. However, the critical point is that the PHP process itself has write access to this directory.

Mitigation Strategy: Encrypted Sessions and Secure Storage

To mitigate the risk of session hijacking via unencrypted session files, we implemented a multi-pronged approach:

1. PHP Session Encryption:

The most direct method is to encrypt the session data *before* it’s written to the file. PHP doesn’t have a built-in mechanism for this with the `files` handler, but it can be achieved by implementing a custom session handler or by using a library that provides this functionality. For this enterprise application, we opted for a custom handler to maintain control and integrate with existing security libraries.

The core idea is to intercept the `session_write_close()` or `session_destroy()` calls and encrypt the session data using a strong symmetric encryption algorithm (like AES-256-GCM) with a securely managed key. The key should *never* be hardcoded in the application or configuration files. It should be loaded from environment variables or a secure secrets management system.

Here’s a simplified conceptual example of a custom session save handler that encrypts data:

class EncryptedSessionHandler implements SessionHandlerInterface {
    private $savePath;
    private $key;
    private $cipher = 'aes-256-gcm';

    public __construct(string $savePath, string $key) {
        $this->savePath = $savePath;
        $this->key = $key;
    }

    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| false {
        $filePath = $this->savePath . '/' . $id;
        if (!file_exists($filePath)) {
            return '';
        }
        $encryptedData = file_get_contents($filePath);
        if ($encryptedData === false) {
            return false;
        }
        return $this->decrypt($encryptedData);
    }

    public function write(string $id, string $data): bool {
        $encryptedData = $this->encrypt($data);
        if ($encryptedData === false) {
            return false;
        }
        $filePath = $this->savePath . '/' . $id;
        return file_put_contents($filePath, $encryptedData) !== false;
    }

    public function destroy(string $id): bool {
        $filePath = $this->savePath . '/' . $id;
        if (file_exists($filePath)) {
            unlink($filePath);
        }
        return true;
    }

    public function gc(int $maxLifetime): int| false {
        $iterator = new DirectoryIterator($this->savePath);
        $now = time();
        $deletedCount = 0;

        foreach ($iterator as $fileinfo) {
            if ($fileinfo->isDot() || !$fileinfo->isFile()) {
                continue;
            }
            $filePath = $fileinfo->getPathname();
            $filemtime = filemtime($filePath);
            if ($filemtime !== false && ($now - $filemtime) > $maxLifetime) {
                unlink($filePath);
                $deletedCount++;
            }
        }
        return $deletedCount;
    }

    private function encrypt(string $plaintext): string| false {
        $ivlen = openssl_cipher_iv_length($this->cipher);
        if ($ivlen === false) {
            error_log("Failed to get IV length for cipher " . $this->cipher);
            return false;
        }
        $iv = openssl_random_pseudo_bytes($ivlen);
        $tag = null; $crypted = null;

        $ciphertext = openssl_encrypt(
            $plaintext,
            $this->cipher,
            $this->key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );

        if ($ciphertext === false) {
            error_log("OpenSSL encryption failed: " . openssl_error_string());
            return false;
        }

        return base64_encode($iv . '::' . $tag . '::' . $ciphertext);
    }

    private function decrypt(string $data): string| false {
        $parts = explode('::', base64_decode($data));
        if (count($parts) !== 3) {
            error_log("Invalid encrypted session data format.");
            return false;
        }
        list($iv, $tag, $ciphertext) = $parts;

        $plaintext = openssl_decrypt(
            $ciphertext,
            $this->cipher,
            $this->key,
            OPENSSL_RAW_DATA,
            $iv,
            $tag
        );

        if ($plaintext === false) {
            error_log("OpenSSL decryption failed: " . openssl_error_string());
            return false;
        }
        return $plaintext;
    }
}

// Usage in your application's bootstrap or configuration file:
$sessionSavePath = '/var/lib/php/sessions'; // Ensure this directory exists and has correct permissions
$encryptionKey = getenv('SESSION_ENCRYPTION_KEY'); // Load from environment variable

if (!$encryptionKey) {
    die("SESSION_ENCRYPTION_KEY is not set.");
}

$handler = new EncryptedSessionHandler($sessionSavePath, $encryptionKey);
session_set_save_handler($handler, true);
session_start();

Key considerations for the custom handler:

  • Key Management: The `$encryptionKey` must be a strong, randomly generated key. It should be stored securely, ideally using a secrets management service (like HashiCorp Vault, AWS Secrets Manager, or DigitalOcean’s App Platform Secrets) and injected into the application’s environment.
  • Algorithm Choice: AES-256-GCM is recommended for its authenticated encryption, providing both confidentiality and integrity.
  • Error Handling: Robust error logging is essential for debugging encryption/decryption failures.
  • Session Garbage Collection: The `gc` method needs to be compatible with the encrypted files. The current implementation relies on file modification times, which should still be valid.

2. Secure Session Storage Location:

Even with encryption, it’s best practice to store session files in a location inaccessible by the web server’s public-facing directories. The default `/var/lib/php/sessions` is generally acceptable if properly permissioned. However, for enhanced security, consider:

  • Dedicated User/Group: Running PHP-FPM under a dedicated, least-privilege user and group, and ensuring the session save path is only writable by this user/group.
  • SELinux/AppArmor: Implementing mandatory access control policies to further restrict access to the session directory.
  • External Session Stores: For very high-traffic or distributed environments, consider using external, managed session stores like Redis or Memcached. These often offer built-in encryption and better performance characteristics.

For this specific DigitalOcean setup, we ensured the `session.save_path` was set to a directory like `/var/local/php_sessions` and that this directory was owned by the PHP-FPM user/group (e.g., `www-data`) with strict permissions (e.g., `700` or `750` if group access is needed for other administrative tasks).

sudo mkdir -p /var/local/php_sessions
sudo chown www-data:www-data /var/local/php_sessions
sudo chmod 700 /var/local/php_sessions

Then, update `php.ini`:

session.save_path = "/var/local/php_sessions"

And restart PHP-FPM:

sudo systemctl restart php[version]-fpm

Securing the Encryption Key

The security of the entire encrypted session mechanism hinges on the secrecy of the encryption key. Hardcoding it is a cardinal sin. Environment variables are a step up, but for production systems, a dedicated secrets management solution is paramount.

DigitalOcean App Platform Secrets: If the application is deployed on DigitalOcean’s App Platform, its built-in secrets management is the ideal place to store the session encryption key. This key is then injected as an environment variable into the running application container.

External Secrets Manager (e.g., HashiCorp Vault): For more complex deployments or on standard Droplets, integrating with a tool like HashiCorp Vault is recommended. The application would authenticate with Vault (e.g., using a service token or cloud-specific authentication) and retrieve the encryption key at runtime.

Example using environment variables (for demonstration):

# On the server, before starting the application or web server
export SESSION_ENCRYPTION_KEY=$(openssl rand -base64 32)

This command generates a 256-bit (32-byte) random key and sets it as an environment variable. This variable must be available to the PHP process. For PHP-FPM, this often involves configuring `php-fpm.conf` or using systemd service files to pass environment variables.

Verification and Ongoing Monitoring

After implementing the encryption handler and securing the storage path, rigorous verification is essential:

  • Functional Testing: Ensure all application features relying on sessions (login, user state, shopping carts, etc.) function correctly.
  • Session File Inspection: Manually inspect session files in the save path. They should now contain seemingly random, unreadable binary data (base64 encoded).
  • Security Audits: Re-run targeted security scans and penetration tests focusing on session management.
  • Log Analysis: Monitor application and server logs for any new errors related to session handling or encryption.
  • Traffic

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

  • Step-by-Step: Diagnosing indexing lock conflicts and high CPU during bulk stock updates on DigitalOcean Servers
  • How to Debug and Fix memory leaks and socket exhaustion in daemon processes in Modern C++ Applications
  • Infrastructure as Code: Provisioning Secure PHP Clusters on DigitalOcean Using Terraform
  • Fixing Slow Largest Contentful Paint (LCP) caused by unoptimized database queries in Legacy Laravel Codebases Without Breaking API Contracts
  • An Auditor’s Checklist for Securing Laravel Backends on Google Cloud

Copyright © 2026 · Vinay Vengala