跪拜 Guibai
← Back to the summary

PHP's 9.2-CVSS RCE: Installing the Patch Is Not the Same as Running It


theme: fancy

Yesterday two severe remote code execution (RCE) vulnerabilities in PHP were disclosed, numbered CVE-2026-109618 and CVE-2026-109619, with a CVSS score of 9.2. According to the official advance notice and the EU Computer Emergency Response Team (CERT-EU) advisory, the patch release window is set for 22:00 Beijing time on October 21.

PHP vulnerability

The news triggered broad discussion in the developer community, and even drew a tweet from Japanese Prime Minister Sanae Takaichi (which developer on this earth gets this treatment? PHP wins completely).

Takaichi tweet

In hundreds of comments on the Hacker News hot thread, engineering teams focused on two things. One is the time gap between distributions building, validating, and distributing patch packages. The other is that many old PHP projects still running have no upstream official patch support, while jumping directly across major versions carries very high risk of code modification and extension compatibility problems.

ServBay has completed security patch updates for this PHP vulnerability and also released patch support for older PHP versions. For developers maintaining multi-version projects or legacy systems, having the patch package ready solves the ammunition problem; the most critical next action is verifying whether the updated binaries are actually running on every target site.

When a PHP security patch is applied, a terminal upgrade command that prints success does not mean live sites and background jobs are out of danger. When multiple PHP versions exist on the same server or development machine, the CLI, the web request handling process (PHP-FPM), and resident-memory queues each load independent binaries and configuration files. Only by collecting runtime evidence through the four links, package installed, site selected, process switched, and regression passed, can you confirm that the new patch has truly taken over business traffic.

Why installing is not the same as running

Many developers are used to having AI coding assistants generate upgrade commands. After running the package upgrade command in the terminal and seeing the latest version number printed, they assume the fix is complete. In multi-version coexistence environments, this kind of judgment easily leaves gaps in defense.

Multi-version coexistence

PHP on a machine has at least three independent execution paths.

Terminal CLI entry

When a developer types php -v in an SSH session or local terminal, the operating system finds the executable based on the current user's PATH environment variable. After the package update, the default symlink may point to the new version, but this only changes command behavior for the current terminal session and has no direct connection with the processes serving HTTP.

Web server forwarding entry (PHP-FPM)

When Nginx and Apache handle dynamic requests, they forward the requests through the fastcgi_pass directive to the backend PHP-FPM listening port (such as 127.0.0.1:9000) or a Unix Domain Socket (such as /run/php/php8.2-fpm.sock).

There are two typical disconnects here.

The first is cross-branch coexistence: for example, the system has PHP 8.3 installed, but the site's Nginx configuration still connects to php8.1-fpm.sock, so external traffic continues to be handled by the old branch.

The second is a same-branch minor version update: although the binary on disk has been replaced, if the running php-fpm master process has not received a reload or restart signal, the worker processes in memory will continue executing the old program.

Background resident processes (Queue Worker)

Projects using Laravel Queue, Symfony Messenger, Swoole, or RoadRunner have background processes managed by Supervisor or Systemd and resident in memory for long periods. Even if both the CLI and the web PHP-FPM have been updated, unless an explicit restart command is sent to the resident workers, queue consumers and scheduled tasks still run in the old version's memory image.

Four-step closed-loop verification process

To prove that the patch is actually effective in production or test environments, do not just rely on the installation success message. Runtime evidence must be collected in four steps.

Step 1: Inventory the PHP versions installed on the machine

Before and after the upgrade, inspect all PHP CLI and FPM binary paths on the machine to understand the environment baseline.

# List all executables named php in the environment variables
which -a php

# Show the current default CLI's detailed version and build time
php -v

# Find all running php-fpm processes and their configuration files
ps aux | grep php-fpm

If you use a Linux system package manager, check the installed package names and specific build versions with the following commands to confirm that the packages containing the CVE-2026-109618 and CVE-2026-109619 fixes have been downloaded.

# Debian / Ubuntu: check installed PHP-related package versions
dpkg -l | grep -E "php[0-9]+.[0-9]+-(cli|fpm)"

# RHEL / Rocky / AlmaLinux: check installed PHP packages
rpm -qa | grep -E "php-(cli|fpm)"

Step 2: Verify the target site's FastCGI binding configuration

Do not assume the upgrade command automatically modifies the web server configuration. Open the target site's Nginx or Apache virtual host configuration file and locate the real address pointed to by fastcgi_pass or SetHandler.

# Search all sites' fastcgi_pass targets in the Nginx config directories
grep -rn "fastcgi_pass" /etc/nginx/conf.d/ /etc/nginx/sites-enabled/

After getting the Socket path or port number, reverse-look up the PID of the process listening on that address and the real path of the corresponding binary.

# Find the corresponding php-fpm master process PID through the Socket or port
sudo lsof /run/php/php8.3-fpm.sock

# Check the actual binary path loaded by this PID (replace 12345 with the actual PID)
sudo ls -l /proc/12345/exe

On Linux, if the binary on disk has been updated but the process has not restarted, the end of /proc/PID/exe shows (deleted). That is direct evidence that the disk has been updated while memory is still running the old version.

Step 3: Restart services and background resident processes

After confirming that the site configuration points to the correct PHP branch, restart the corresponding PHP-FPM service and background queue processes in order.

# Restart the corresponding PHP-FPM service (replace with the actual version number)
sudo systemctl restart php8.3-fpm

# Check the service start timestamp and the new master PID
sudo systemctl status php8.3-fpm

For PHP projects containing asynchronous queues or resident-memory frameworks, run the framework-level and process manager refresh commands next.

# Laravel: notify all queue workers to exit and reconnect after the current job
php /var/www/project/artisan queue:restart

# If Supervisor manages resident tasks, restart the corresponding process group
sudo supervisorctl restart all

Step 4: Controlled runtime verification and business regression

To verify which PHP version is actually effective on the web side, do not leave a publicly accessible phpinfo.php file in the website root for a long time; doing so leaks server environment variables and path structure to the public network. A safer approach is to use a one-time minimal script under controlled access limited to the local loopback address (127.0.0.1), output the runtime version and SAPI information, and delete it immediately after verification.

# 1. Create a temporary verification script readable only by localhost requests
cat << 'EOF' > /var/www/project/public/.patch_check.php
<?php
if ($_SERVER['REMOTE_ADDR'] !== '127.0.0.1' && $_SERVER['REMOTE_ADDR'] !== '::1') {
    http_response_code(403);
    exit('Forbidden');
}
header('Content-Type: application/json');
echo json_encode([
    'sapi' => PHP_SAPI,
    'version' => PHP_VERSION,
    'version_id' => PHP_VERSION_ID,
    'ini_loaded' => php_ini_loaded_file(),
    'opcache_enabled' => function_exists('opcache_get_status') && opcache_get_status(false) !== false,
], JSON_PRETTY_PRINT);
EOF

# 2. On the server itself, use curl with a Host header to send the verification request
curl -s -H "Host: example.com" http://127.0.0.1/.patch_check.php

# 3. After confirming the version matches the expected patch version, destroy the temporary file immediately
rm -f /var/www/project/public/.patch_check.php

When the returned JSON shows sapi as fpm-fcgi and version matches the fixed version, the web path has truly switched over. Then check that core extensions (redis, pdo_mysql, bcmath, opcache) load normally, and run regression tests on core interfaces.

Old-version patches and per-project switching

In actual business maintenance, many teams face a dilemma. On one hand, they maintain new projects running on PHP 8.3 and legacy systems running on PHP 7.x or even earlier versions. On the other hand, many traditional software sources provide updates only for active branches. If an old project is forced to jump across major versions, large amounts of legacy syntax and old extensions will fail immediately.

To address this, ServBay not only applied security patches to current mainstream PHP branches as a priority, but also released vulnerability fix support specifically for older PHP versions. Combined with ServBay's per-project PHP version selection isolation mechanism, maintainers can complete security hardening without breaking the code compatibility of old projects.

PHP multi-version

In mixed multi-version environments, this governance approach solves three engineering problems.

Upgrade verification checklist and common Q&A

To make daily maintenance and team collaboration easier, the whole set of verification actions can be fixed as a standard checklist. Go through each item every time a PHP security update or version switch is performed.

Multi-version PHP environment upgrade verification table

Verification stage Check item Verification method and tools Pass criterion
Package installed Patch package version check Official security advisory or ServBay package panel Mainstream version or legacy support package has been updated to a build containing the patch
Package installed CLI entry version check which -a php and php -v Terminal path points to a patched binary
Site selected Web forwarding configuration check Inspect Nginx fastcgi_pass or site binding settings The PHP version bound to the site matches the target patched branch
Process switched PHP-FPM process refresh Restart the FPM service and check /proc/PID/exe Master PID has been updated and no (deleted) remains
Process switched Resident queue process refresh artisan queue:restart / supervisorctl All worker process start times are later than the upgrade time
Regression passed Web runtime evidence collection Controlled localhost access to a one-time minimal script (burn after reading) Returned PHP_VERSION exactly matches the patched version
Regression passed Extension and business interface regression php -m comparison and automated tests on core business interfaces No missing extensions, no new errors in logs, normal responses

FAQ

Why does php -v in the terminal show the latest version, but the PHP_VERSION output on the web side is still the old version?

Because the terminal command calls the PHP CLI program pointed to by the system PATH environment variable, while web requests are forwarded by Nginx or Apache to the PHP-FPM daemon in the background. If the PHP-FPM service has not been restarted, or the site's FastCGI configuration still binds to the old version's Socket, the web side will keep running the old version.

A legacy project under maintenance cannot upgrade to PHP 8.3. How should this high-risk vulnerability be fixed?

Jumping a legacy project across major versions easily causes syntax incompatibility and third-party library errors. For historical projects that cannot be refactored immediately, you can use an environment such as ServBay, which has provided security patch support for older PHP versions, update the patch package directly on the original version branch, and restart the service. This fixes the high-risk vulnerability while keeping the original business logic and extensions running normally.

After updating the security patch on the same branch, why are background queue tasks still not effective?

Background tasks such as Laravel Queue, Swoole, or Workerman are resident-memory processes. They loaded the old PHP interpreter into memory at startup. Updating the PHP package on disk or restarting PHP-FPM will not interrupt resident queue processes. You must separately run php artisan queue:restart or restart the corresponding worker process group through Supervisor so that background tasks load the patched new version.

Summary

When dealing with high-risk vulnerabilities such as CVE-2026-109618 and CVE-2026-109619, downloading and installing the new version is only the starting point; confirming that the running processes have actually loaded the new patch is the finish line. In a multi-version coexistence environment, relying on a single php -v command is easily misled by superficial results.

A stable upgrade strategy has two parts. On the supply side, use tools such as ServBay that provide security patches for both mainstream versions and historical old versions and support binding a PHP version per project, solving the problems of old projects that cannot jump versions and messy multi-site configuration. On the rollout verification side, strictly follow the four-step checklist of package installed, site selected, process switched, and regression passed, and separately confirm the real process state of the CLI, PHP-FPM, and resident queues. Making this verification routine part of the team's daily operating norms means future runtime security updates can be handled with evidence and land smoothly. Rules must be established, then writing code will not give you heartburn.