PHP2Shell and the Patch-Operations Playbook: Rolling an Emergency Fix Across a Real Fleet

# php# security# devops# webdev
PHP2Shell and the Patch-Operations Playbook: Rolling an Emergency Fix Across a Real FleetNguyen Tu

The most-read story on daily.dev today is not a new framework or a benchmark — it is an emergency: on...

The most-read story on daily.dev today is not a new framework or a benchmark — it is an emergency: on October 21, 2026, the PHP project shipped fixes for two critical remote code execution flaws, CVE-2026-109618 and CVE-2026-109619, nicknamed PHP2Shell. Both score 9.2 on CVSS v4.0, and an unauthenticated attacker can run code on a vulnerable server. The news cycle covered the bug itself. This article is the practitioner's version — the problem we actually solve for clients: the fix exists, but does it run everywhere it needs to? The distance between "released" and "deployed" is where incidents happen, and closing that distance is pure automation work. It is the same philosophy as how we work: automate the routine aggressively so human judgment is spent exactly where it changes the outcome.

1. Released is not deployed: the uncomfortable math

The patched releases are PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 (released October 21, 2026, at 14:00 UTC — pulled forward a day after the CVE IDs leaked). The PHP team also published minimal source backports for 7.4, 8.0 and 8.1, which receive no official release.

Now the uncomfortable part: Bitsight's internet-wide scans found that only about a third of the PHP instances where the version could be identified run a branch covered by the security update. The rest sit on older branches, and the most common branch by far is PHP 7.4, with more than two million observations. So for a large share of the real world, "the fix is out" and "we are fixed" are two different sentences. Your job on a day like this is to turn the second one into truth, systematically, before someone else turns it into a breach.

The two flaws themselves sit in the engine's memory management and error handling — code nearly every request touches. CVE-2026-109618 is OOM corruption: after a memory allocation fails, PHP leaves invalid data behind for its cleanup routine to process. CVE-2026-109619 is stale global state: request-owned state survives after its memory is freed, and a later request on the same worker can read or write through the dangling pointers. Every setup serving web requests is exposed — PHP-FPM, mod_php, php-cgi, FrankenPHP, LiteSpeed — with mod_php rated highest risk because the exploit fires before any application code runs.

2. Inventory: find every PHP runtime you own

You cannot patch what you do not know runs PHP. On emergency-patch day, the inventory step is the one most teams skip, and it is the one that decides whether the patch actually covers the fleet. A real inventory pulls from several sources of truth, because PHP hides in all of them:

  • Container images: Dockerfile and compose files that pin a PHP image tag. This is the fastest place to scan first, because it is text.
  • Hosts: packages installed via apt/dpkg or rpm/yum, plus anything compiled from source.
  • Running processes: what the package manager says and what actually serves traffic can disagree.
  • Serverless and PaaS: runtimes managed by someone else — some vendors auto-patched (Google Cloud patched supported serverless runtimes; Laravel Cloud auto-patches), but custom container images and App Engine flexible deployments need a manual redeploy.
  • Hosts you do not control: shared hosting, where the fix depends on the provider. Updating WordPress, plugins or themes does not change your PHP version, because PHP lives on the host — check php -v on every server, and ask your host when it will patch if you do not control it.

A first-pass scan can be embarrassingly simple and still catch most of the fleet:

# 1. Every container image pinning a PHP base image
grep -rn "FROM php:" --include='Dockerfile*' --include='docker-compose*' .

# 2. Every host with PHP installed (Debian/Ubuntu fleet via your config management)
dpkg -l | grep -E '^ii\s+php[0-9]' | awk '{print $2, $3}'

# 3. What is actually running right now (catches source builds and stray SAPIs)
ps aux | grep -E 'php-fpm|apache2|httpd|litespeed' | grep -v grep

# 4. What version answers the web (spot-check the externally visible truth)
for host in web1 web2 web3; do
  ssh "$host" 'php -v | head -1'
done
Enter fullscreen mode Exit fullscreen mode

For codebases at scale, this is exactly what Sourcegraph published search queries and batch-change specs for during this event: finding Dockerfiles, CI pipelines and Helm charts that pin old PHP versions, then proposing the bump as a batch change. We have written about this machinery before — our guide to Git workflows for agent teams covers batch changes and reviewable, revertable rollouts, which is the same shape of work: many small, verified, reviewable changes instead of one heroic push.

3. The restart problem: upgrading the package is not the fix

This is the detail that eats patches in production. On Ubuntu, fixes exist for all supported releases — but running apt upgrade does not finish the job, because unattended upgrades do not restart running applications. The old, vulnerable PHP-FPM workers keep serving traffic until you restart them. The same is true for Apache with mod_php. A fleet that is "fully patched" in the package database and still vulnerable in memory is the classic failure mode of emergency patching.

The restart semantics differ by platform, and on patch day you need the real list, not assumptions:

Platform / SAPI What actually applies the fix
Ubuntu/Debian PHP-FPM apt upgrade + restart php*-fpm and the web server; unattended-upgrades will not restart running apps
mod_php (Apache) Restart Apache; highest risk SAPI, since the exploit fires before application code runs
Laravel Cloud Auto-patches
Laravel Vapor (PHP 8) Apps redeployed automatically
Laravel Vapor (PHP 7) No patch at all — the app must be upgraded off the branch
Laravel Forge Manual update under Runtime → Versions
Google Cloud serverless Supported runtimes auto-patched; custom images need a manual redeploy
Cloudways WordPress instances auto-patched
DigitalOcean Droplets / DOKS / App Platform Manual action required

So the rollout script has two halves — the version bump and the restart — and the second half is where verification must happen:

#!/bin/bash
# bump-and-restart.sh — the boring half that actually matters
set -euo pipefail
sudo apt update && sudo apt upgrade -y php8.4-fpm
# Restart the runtime AND the web server; the old workers stay in memory otherwise
sudo systemctl restart php8.4-fpm
sudo systemctl restart apache2 || sudo systemctl restart nginx
# Verify the fix is the thing actually running, not just installed
php -v | head -1
Enter fullscreen mode Exit fullscreen mode

The principle generalizes beyond PHP: a patch is only real when the vulnerable code is no longer in memory. Anything in your rollout that checks the package database but not the running process is checking the wrong thing.

4. Rollout mechanics: canary, stages, re-runnable automation

Emergency does not mean reckless. A staged rollout with a canary still beats a fleet-wide push on a CVE day, because the patch itself is a change to every request path in your application, and engine-level patches have historically broken extensions and edge-case behavior. The mechanics that work:

  1. Patch one canary host or one non-critical app first, watch error rates and latency for a full traffic cycle, then proceed.
  2. Make the rollout re-runnable (idempotent). The same script run twice must converge to the patched state, not fail or double-apply. On a day when you are patching at 2 a.m. with tired operators, re-runnable beats clever.
  3. Keep it reversible. Engine patches occasionally break third-party extensions; know how to roll back to the previous package version before you start, not after.
  4. Batch the config changes. Dockerfile pins, CI pipeline images and Helm chart tags should be bumped as reviewable changes with CI green — not hand-edited on servers. This is where the batch-change approach from our agent Git workflows post earns its keep: dozens of small, verified, revertable changes instead of snowflake servers.

5. The EOL fleet: what to do with 7.4, 8.0 and 8.1

A large share of the internet runs PHP branches that get no official release — 7.4 alone had more than two million observations in Bitsight's scans, and W3Techs data puts about 48.5% of PHP sites on end-of-life branches. The PHP project published minimal source patches for 7.4, 8.0 and 8.1 to help with backports, but that is not a supported release; your vendor's security advisory is the authority on whether a patched package exists for your platform.

Be honest about the decision tree:

  • Vendor package exists: take it, then restart (section 3 still applies).
  • No package, and you can upgrade the branch: schedule it — this event is the forcing function.
  • No package and no upgrade path right now: this is where CERT-EU, the NSA and CISA landed — isolate those systems or take them offline. Several governments did exactly that: Dutch government services, Finland's state IT provider, German and French federal IT all pulled sites down rather than run exposed.

"Awaiting the vendor" is not a plan. A written decision with a date — patched, isolated, or offline — is.

6. Defense in depth while the rollout runs

Patching a fleet takes time: inventory, maintenance windows, application testing against the new builds. During that window, boundary controls reduce exposure to the confirmed attack paths. Cloudflare turned on default WAF rules for its customers about nine hours before disclosure, after attackers began probing, and AWS published a guide with AWS WAF rules for the confirmed PHP2Shell attack paths. A reverse proxy or WAF in front of PHP can block hostile requests before they reach the vulnerable parser.

Two caveats, both worth stating plainly:

  • A WAF does not fix PHP. It covers known attack patterns; the vulnerable code is still underneath. Patching is the only complete fix, and it stays priority one.
  • Expect payload mutation. Cloudflare reported attackers already adapting payloads to evade its rules. Static rules buy time; they do not end the race.

Detection matters as much as blocking. Datadog's analysis suggests looking for large form submissions, memory-exhaustion errors during request initialization, and unexpected shell processes spawned by the web server user — and notes that any PHP script exposes the vulnerable path, even one that only prints a response, because the request never reaches application code. Practical log checks:

# Requests hitting the session upload-progress path (the reported trigger surface)
grep -E 'PHP_SESSION_UPLOAD_PROGRESS' /var/log/apache2/access.log | tail -20

# The literal calling card some scanners used
grep -i 'php2shell' /var/log/nginx/access.log | awk '{print $1}' | sort -u

# Shell processes that should never be children of the web server user
ps -ef | awk '$1=="www-data" && /sh|bash|wget|curl/ {print}'
Enter fullscreen mode Exit fullscreen mode

Arctic Wolf Labs reported scanning and attempts pulling ELF and Golang malware binaries via wget — so unexpected outbound fetch-and-execute behavior from web-server-owned processes is exactly the signal to alert on. (On the bot-detection side of boundary defense, we have a full breakdown of TLS fingerprints and token-based bot detection — the same layer where these WAF rules live.)

7. Verify the patch actually landed

The rollout is not done when the script exits green. It is done when every runtime in the inventory answers with a fixed version. Close the loop:

#!/bin/bash
# verify-php-fleet.sh — the inventory from section 2, re-run as an assertion
FIXED=("8.5.12" "8.4.27" "8.3.36" "8.2.35")
for host in $(cat fleet-hosts.txt); do
  ver=$(ssh "$host" 'php -r "echo PHP_VERSION;"')
  ok="no"
  for f in "${FIXED[@]}"; do [[ "$ver" == "$f" ]] && ok="yes"; done
  # Also confirm the running workers are the new build, not stale memory
  echo "$host -> php $ver | patched: $ok"
done | tee patch-verification-$(date +%F).log
Enter fullscreen mode Exit fullscreen mode

Keep that log. When the next CVE drops — and Taylor Otwell's warning during this event was that this probably will not be the last time, nor PHP the only language to go through it — the difference between a two-hour response and a two-day scramble is whether your inventory already exists. The fleet list, the restart runbook and the verification script are assets. Build them once, on a calm day, and CVE day becomes execution instead of archaeology.

8. Frequently asked questions

Does updating WordPress, my plugins or my theme fix PHP2Shell?
No. The vulnerability is in the PHP runtime itself, which lives on the host — below your application. Update the runtime (and restart it), regardless of what your CMS or framework reports.

Does putting a WAF in front fix it?
No. WAF rules from Cloudflare, AWS and others reduce exposure to confirmed attack paths while you patch, but they do not remove the vulnerability. Patching is the only complete fix.

I run PHP 7.4. Am I covered?
Not by an official release. The PHP project published minimal source patches for 7.4, 8.0 and 8.1 to aid backports; check your OS vendor's security advisory for patched packages. If neither exists, the official guidance from CERT-EU, NSA and CISA is to isolate the system or take it offline — or upgrade the branch.

How do I know if someone already exploited it against me?
Look for the signals in section 6: large form submissions, memory-exhaustion errors during request initialization, unexpected shell processes or wget/curl activity owned by the web-server user, and php2shell strings in access logs. Absence of these signals is reassuring, not proof.

Why is this suddenly everywhere?
Both flaws were found with frontier AI assistance — Calif's team with OpenAI's models, and Wiz independently with its Atlas tool — and one researcher demonstrated that a working proof of concept was roughly "one prompt away" once hints leaked. The finding side of security is accelerating; the patching side has to keep up, which is exactly what the playbook above is for.

References


Originally published on botslave.com. Botslave is an automation studio — see what we build and how we work.