Responding to the PHP zero-day across 173-plus websites

On 21 October 2026, we updated and patched more than 173 websites in response to the critical PHP security release. Many belonged to older projects that had been running since well before I joined the company. Alongside the websites, there was manual work across three DigitalOcean servers, with Ubuntu as our main operating system.

We moved the estate to PHP 8.3 as a minimum and reached the patched PHP 8.3.36 release. We also took ecommerce systems and systems containing private payment information or sensitive customer data offline as a precaution while the work was carried out.

The difficult part was the combination of scale, older application code and the need to make decisions quickly. Updating a familiar website is one job. Establishing what is running across a large collection of projects, deciding which services should stay exposed and checking that an upgrade has actually taken effect is a much larger one.

What the PHP security issue actually was

The alert commonly described as a PHP zero-day concerned two critical vulnerabilities in the PHP runtime: CVE-2026-109618 and CVE-2026-109619. They could allow remote code execution without an attacker first signing in. The PHP security advisory covered web-serving installations including PHP-FPM, Apache mod_php, php-cgi, FrankenPHP and LiteSpeed. The precise attack paths differ between configurations.

CVE-2026-109618 concerns memory corruption during out-of-memory handling. PHP can leave internal request data only partly initialised, then attempt to use it during cleanup. In the demonstrated mod_php case, this could happen before application code ran. A login screen or a well-maintained CMS therefore could not be treated as a complete defence.

CVE-2026-109619 concerns stale internal state surviving after the memory belonging to a request has been freed. A later request handled by the same worker can encounter that state. These are engine-level problems involving request handling, memory and errors, rather than a flaw confined to one plugin or one website feature.

The PHP Foundation’s account of the release explains that disclosure was brought forward after the CVE identifiers began circulating on 20 October. The fixes were released on 21 October. That shortened the preparation window and made checking the official release information particularly important.

Why we treated it as urgent

Remote code execution changes the consequences of leaving a site exposed. An attacker running code with a PHP worker’s permissions may be able to read application configuration, use database credentials, access records or alter files that the worker can write. It does not automatically mean root access, but an application account can already reach a great deal of valuable information.

For ecommerce, I was concerned about customer records, order integrity, checkout behaviour and the credentials used to connect services. The payment arrangement matters: using a hosted gateway does not mean that the shop stores full card details. It also does not remove the risks to the shop’s own customer data or to a checkout page that an attacker might modify. Those were reasons to treat the whole application seriously.

The server mattered as well. Separate websites can still share a machine, a service account or readable configuration. The potential reach of a compromised application depends on those boundaries. Counting domains is useful for tracking the work, but it does not tell you how many distinct PHP processes or security boundaries you have.

We took ecommerce and sensitive-data systems offline while patching. That was a deliberate choice about exposure and the consequences of getting it wrong. The joint European security statement advised prioritising internet-facing systems and considering isolation from public networks where immediate remediation was not possible.

For this kind of issue, an application maintenance page is only useful if it actually prevents vulnerable PHP from receiving requests. A static response at the web server or reverse proxy can provide that boundary; a maintenance plugin that still boots PHP cannot be assumed to do so. The route into the application matters as much as the message visitors see.

The patch version mattered more than the branch label

PHP 8.3 was our minimum baseline, but PHP 8.3 by itself was not the fix. An installation on 8.3.35 still needed attention. The upstream fixed releases announced on 21 October were:

PHP branchFixed upstream release
8.28.2.35
8.38.3.36
8.48.4.27
8.58.5.12

These are the upstream release numbers. Distribution vendors may backport security fixes into packages with a different-looking version string, so their security notices and complete package revisions matter too. For our PHP 8.3 work, the confirmed destination was 8.3.36.

There was also a longer-term reason to move older projects forward. PHP 8.1 and earlier were already outside normal upstream support. PHP’s support schedule puts PHP 8.3 in security-only support through the end of 2027. That made it a practical floor for this work, with further upgrades still part of ongoing maintenance.

1. Identify websites and the services behind them

At this scale, the first useful document is an inventory. The fields I need are the domain, hosting account or server, application, current PHP handler, PHP version, responsible person and whether the service handles payments or sensitive customer information. A status column then gives each system a clear place in the response: identified, isolated where necessary, updated and checked.

Older projects deserve particular attention because the assumptions around them can be out of date. A site may have moved host, acquired a different PHP handler or retained an old dependency that nobody has had to touch recently. I would also include subdomains, staging sites, old administration tools and background services in the search. A forgotten application can still be reachable.

2. Check the PHP used by real requests

The shell is a useful starting point, but php -v reports the command-line interpreter. A website can use another version through FPM, Apache, a hosting-panel setting or a container. That distinction is especially important on a machine hosting several generations of applications.

The following commands are illustrative Ubuntu checks, not an exact transcript from our three servers. Package names, service names and enabled repositories need to match the machine being inspected.

php -v
php --ini
dpkg-query -W -f='${binary:Package}\t${Version}\n' 'php*'
systemctl list-units --type=service --all 'php*-fpm.service'

The dpkg-query manual documents the package inventory format. I would also trace each virtual host to its configured FPM socket or Apache handler, then confirm the version through a restricted diagnostic or the hosting provider’s runtime view. A public phpinfo() page is unnecessary and can expose configuration details.

3. Establish recovery and compatibility before changing the runtime

A patch response still needs a way back from a failed change. Before an upgrade, I want current database and file backups, the relevant configuration and a record of the runtime being replaced. For a shop, that also means understanding orders or other writes that might arrive during the work. Restoring an old database over new orders would create another incident.

Moving a much older project onto PHP 8.3 adds compatibility work to the security update. Themes, plugins, custom code and extensions can behave differently. My earlier PHP compatibility work follows the same principle: inspect the application, test the important journeys and have a recovery route that does not leave an unpatched service publicly exposed.

4. Refresh repositories and apply the fixed packages

The manual server work covered three separate servers. A successful update on one machine did not establish the state of the others. Each needed its own package and service checks. The same applies to hosting-panel sites: selecting a PHP branch only helps when the provider’s build for that branch contains the fix.

For an existing, package-managed PHP 8.3 FPM installation on Ubuntu, an example is to refresh the package index, inspect the candidate versions and simulate the package change before applying it. APT’s manual distinguishes refreshing the index from installing upgraded packages.

sudo apt-get update
apt-cache policy php8.3-cli php8.3-common php8.3-fpm
sudo apt-get --simulate install --only-upgrade php8.3-cli php8.3-common php8.3-fpm

That example lists the core CLI and FPM packages. The actual upgrade list must also include every installed PHP 8.3 extension that needs updating, such as database, XML, curl, mbstring, ZIP or OPcache packages. Review the proposed changes and the vendor’s security notice. If the repository does not offer a fixed build, do not count a successful package command as remediation.

sudo apt-get install --only-upgrade php8.3-cli php8.3-common php8.3-fpm

The --only-upgrade example deliberately does not migrate a PHP 7 or PHP 8.1 application to 8.3. That requires installing the intended branch and required extensions, checking compatibility, and switching the application’s handler. Changing the default command-line interpreter alone does not switch a website’s FPM socket.

Containers, bundled runtimes and hosting platforms need their own update path. Updating the Ubuntu host does not replace PHP inside a container image. Similarly, a custom-built service may not use the distribution’s FPM unit. The useful question is which executable will handle the next web request.

5. Restart the processes that serve the websites

Replacing packages on disk is not enough if old processes remain alive. For a conventional PHP 8.3 FPM service, an illustrative check and restart sequence is:

sudo php-fpm8.3 -t
sudo systemctl restart php8.3-fpm
systemctl is-active php8.3-fpm
php8.3 -v

The service name must be the one the site actually uses. Apache mod_php, custom FPM services and long-running application workers require their corresponding restart or redeployment. A routine reload of Nginx alone does not replace an old PHP executable. The CLI version check above is useful evidence, but the web runtime still needs its own confirmation.

6. Verify the application before restoring access

Reopening should depend on both the patched runtime and a working application. My checks for a shop would cover product pages, basket behaviour, checkout in an appropriate test mode, account access, order administration and the jobs or callbacks the business depends on. Form-based systems need their validation, file handling and access controls checked as well.

I would review PHP and web-server errors alongside those journeys, record the actual package and runtime versions, and only then mark an inventory entry complete. Logs can help find compatibility problems or suspicious activity, but quiet logs do not prove that a vulnerable system was safe. Patching also does not remove any malicious files or credentials an attacker may already have obtained; evidence of compromise needs a separate incident response.

What I want to carry forward

The result of the day was more than 173 websites updated and patched, older projects brought to a PHP 8.3 minimum, and manual upgrades across three servers. PHP 8.3.36 was the concrete patch target for the 8.3 systems. The temporary withdrawal of ecommerce and sensitive-data services was part of that response.

What stays with me is how much the older estate matters during an urgent update. A website does not stop being our responsibility because the original project predates the current team. Clear ownership, a reliable inventory and supported runtimes make the next decision easier. They also give us a better basis for deciding when a short interruption is the responsible choice.

Technical details and example commands checked on 22 October 2026. The incident and agency response described here took place on 21 October. The linked advisories remain the reference for affected versions and any later updates.

More about me