Swiss...
Patching PHP2Shell Across a Hosting Fleet, Same Day
On 21 October 2026, two critical PHP RCE vulnerabilities went public with working exploits. We patched and verified our entire infrastructure the same day. Here’s how, and why the sequence matters more than the speed.
October 21, 2026
by Ezequiel Pineda 7 min read
Patching PHP2Shell CVE-2026-109618 across a hosting fleet same day

On 21 October 2026, the PHP project released emergency fixes for two critical remote code execution vulnerabilities — CVE-2026-109618 and CVE-2026-109619, nicknamed “PHP2Shell.” Both rated CVSS 9.2. Both unauthenticated. Both reachable through the PHP engine itself, meaning every application serving PHP was exposed regardless of how clean its own code was. Proof-of-concept details leaked before the patch landed, and exploitation began within hours.

We patched and verified our entire infrastructure the same day. Here’s how, and why the sequence matters more than the speed.

The Problem with “Just Patch It”

The instinct on a critical RCE is to run the update and move on. On a real fleet, that instinct fails in two places.

First, the patched build has to actually exist in your repositories. Upstream PHP shipped fixed versions on release day, but distribution and third-party repositories rebuild their packages on their own schedule — sometimes same-day, sometimes a day behind. For several hours, “update PHP” simply had nothing newer to install on some systems. Running the update anyway produced a newer package revision of the same vulnerable version — a false sense of security, which is worse than knowing you’re exposed.

Second, “patch” means different work on different stacks. A modern PHP branch takes a routine point upgrade. An end-of-life branch has no fix at all — you have to migrate to a supported branch, which pulls in application compatibility, extension loaders, and encoded-code constraints.

So the real job isn’t “patch PHP.” It’s: find every PHP surface, close the exposure window while fixes are in flight, then migrate each system correctly and confirm the application still runs.

The Sequence

1. Inventory every PHP surface. Not just the obvious web apps — every service with a PHP front end counts, including admin panels and monitoring dashboards that are easy to forget. The failure mode here is patching the website and leaving a monitoring UI exposed.

2. Close the exposure window before patching. This is the step most people skip, and it’s the one that matters. Where a fixed PHP build wasn’t yet available in our repositories, we restricted the public web ports to our management addresses at the firewall and left everything else running. A vulnerable binary that the public cannot reach is not an open door.

Worth stating plainly: taking an application’s “maintenance mode” toggle doesn’t help here. The maintenance page is itself rendered by PHP — the request still reaches the interpreter, which is all the exploit needs. The cut has to be at the network layer, not the application layer.

3. Patch to a supported, fixed branch. Systems on a current branch took a straightforward point upgrade once the fixed build was published. Systems on an end-of-life branch had to be migrated to a supported one — a larger change that brings application compatibility into play.

4. Verify the application, not just the version. php -v showing the patched version is necessary but not sufficient. The application has to actually load and run on the new engine. We confirmed each service end to end — admin login, background jobs, the real pages — before considering any box done.

5. Reopen. Only once an application was confirmed healthy on the patched engine did the firewall restriction come down. Reopen to a working service, never to a broken one.

The Sharp Edges

A few things that cost real time, documented here because other operators hit the same walls today and found little written down.

Encoded-code loaders are version-locked. Applications protected with a bytecode loader need the loader built for the exact PHP branch you’re moving to. After a branch migration, the old loader fails hard with an undefined-symbol error, and the application white-pages until the matching loader is installed and the configuration points at it. Package maintainers have also quietly moved from version-stamped loader filenames to a single unversioned file — so a configuration line that worked for years silently points at a file that no longer exists. One-line fix, but only once you know to look.

Duplicate loader registration. Installing the loader package can drop its own configuration file alongside a hand-edited one from a previous setup, loading the extension twice and emitting a warning on every CLI and cron invocation. Harmless to function, noisy in logs. Keep the package-managed config, retire the manual one.

Encoded application modules carry their own version floor. On one system, core loaded fine but a single payment module — encoded for the old branch — blocked a section of the admin interface entirely. The fix is per-module: obtain a rebuilt version from the vendor, or retire the module if it’s been superseded. Core being healthy doesn’t guarantee every encoded add-on is.

The Lesson Worth Keeping

Two, actually.

Firewall-first buys you the window. The gap between disclosure and an installable fix is the dangerous interval on any serious RCE. A fleet that can cut public access surgically, per service, rides out that gap without exposure. That capability is worth building before you need it.

End-of-life versions turn an easy patch into a migration. The systems that took longest were the ones on an unsupported PHP branch, where there was no patch to install — only a migration to perform, under incident pressure. Version lifecycle management isn’t housekeeping; it’s the difference between a ten-minute update and an afternoon.

We run our own infrastructure specifically so that on a day like this, the response is ours to execute and ours to verify — start to finish, same day.

Questions about incident response or managed hosting? Get in touch.