PHP2Shell and Drupal: An Emergency Patch-and-Verification Runbook

Tim Rabbetts | October 29, 2026

Drupal site owners have had to respond to two separate critical events in October 2026: a critical Drupal core remote-code-execution advisory on 14 October, followed one week later by two critical PHP vulnerabilities affecting web-facing PHP installations. The second incident matters even to teams that have already updated Drupal, because the vulnerable code sits below the application layer.

On 21 October 2026, the PHP project released PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 to address CVE-2026-109618 and CVE-2026-109619. PHP describes both issues as capable of unauthenticated remote code execution and recommends immediate updating. Drupal's own security team separately advised site owners that updating to a PHP release containing the fixes was the only complete mitigation. ([php.net](https://www.php.net/index.php))

Why this incident is different from a normal Drupal update

A conventional Drupal security release normally starts with the application inventory: identify the affected module or core branch, update the Composer dependency, run database updates, clear caches and test the site. That remains necessary, but it is not sufficient for this incident.

PHP is the execution runtime for Drupal. Depending on the deployment, a request may be handled by PHP-FPM behind Nginx, by Apache with mod_php, by php-cgi, or by another PHP-compatible web runtime. The PHP advisory specifically identifies web-serving installations as exposed and notes that the risk is highest for Apache's mod_php configuration because a demonstrated attack can trigger before application code runs. ([php.net](https://www.php.net/index.php))

That distinction changes the response plan:

  • Updating Drupal does not repair a vulnerable PHP binary.
  • Updating the command-line PHP binary does not necessarily update the PHP-FPM service handling web traffic.
  • Updating one server does not protect another web node, queue worker, cron container or staging environment.
  • Temporary php.ini changes reduce exposure to specific paths but do not replace the security release.
  • A successful patch should be followed by process restarts and verification of the actual runtime serving requests.

The operational goal is therefore broader than “run Composer update”. It is to establish which PHP processes serve each Drupal site, patch every exposed runtime, update Drupal where required, and retain enough evidence to determine whether further incident response is justified.

First response: build an accurate runtime inventory

Begin with an inventory rather than assuming that the PHP version shown by a deployment shell is the version used by the website. A common example is a host running PHP 8.4 for FPM while the command line resolves to PHP 8.2, or a load-balanced cluster where only one node has been patched.

Check the command-line runtime

php --version
php --ini
command -v php

Record the full version, the loaded configuration file and the binary path. The relevant question is not simply whether PHP is “8.x”, but whether the web-serving runtime is one of the fixed builds: PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or a vendor package containing the same fixes. PHP 8.2 is still receiving this fix, but the project states that PHP 8.2 reaches end of life on 31 December 2026, so it should be treated as a short-term destination rather than a long-term platform decision. ([php.net](https://www.php.net/index.php))

Inspect PHP-FPM services

On a systemd-based host, list running PHP services and inspect their executable paths:

systemctl list-units --type=service --all 'php*-fpm.service'

ps -ef | grep '[p]hp-fpm'

systemctl status php8.4-fpm --no-pager

Use the service name appropriate to the host. Do not copy php8.4-fpm blindly into an environment that uses a different package or a containerised service.

For a definitive web-runtime check, create a temporary diagnostic endpoint outside the public document root workflow or expose it only through an authenticated internal route. A minimal PHP file can report the runtime version:

<?php

header('Content-Type: text/plain');

echo PHP_VERSION, PHP_EOL;
echo PHP_SAPI, PHP_EOL;

Deploy this only under controlled access, request it through the same virtual host and load balancer used by the Drupal site, then remove it immediately. Never leave a public phpinfo() page or diagnostic endpoint in production.

Check every execution context

Include all of the following in the inventory:

  • PHP-FPM pools serving public Drupal traffic.
  • Apache modules or CGI handlers.
  • CLI PHP used by Drush and Composer.
  • Queue workers and long-running consumers.
  • Cron containers and scheduled task runners.
  • Build images used by CI/CD.
  • Staging, preview and disaster-recovery environments.
  • Managed hosting instances where the provider controls the operating system.

Long-running workers are particularly easy to miss. A package update may place a fixed binary on disk while an existing worker continues to use the old process image. Restarting the relevant service is therefore part of the patch, not an optional clean-up task.

Apply the PHP security update

The safest route is to use the operating system, cloud image or hosting provider's supported package channel. PHP states that Linux distributions and long-term-support vendors received the patches in advance, so the exact package version may include a vendor suffix rather than matching the upstream version string character for character. ([php.net](https://www.php.net/index.php))

For Debian or Ubuntu, the exact package names depend on the installed PHP branch and repository. A typical package-managed update looks like this:

sudo apt update
sudo apt install --only-upgrade php8.4-cli php8.4-fpm php8.4-common
sudo systemctl restart php8.4-fpm

For Red Hat-derived systems, use the package stream already approved for the host:

sudo dnf upgrade 'php*'
sudo systemctl restart php-fpm

These examples are deliberately conservative. Package names and service names vary between distributions, repository streams and hosting platforms. Confirm the transaction before accepting it, and check the provider's release notes if PHP is supplied through a control panel or managed platform.

After the update, verify both the binary and the service:

php --version
systemctl status php8.4-fpm --no-pager
sudo journalctl -u php8.4-fpm --since '15 minutes ago' --no-pager

Then test a request through the real web path. If the site is load-balanced, repeat the check against every node or use node-specific health checks where available.

Containerised deployments

In a containerised deployment, updating the host's PHP package may have no effect at all. The fix must be applied to the image used by the web service, followed by a rollout that replaces the running containers.

FROM php:8.4.27-fpm

RUN docker-php-ext-install opcache pdo_mysql

The extension list in this example is illustrative rather than a universal Drupal requirement. Preserve the extensions required by the application and base image, but pin the PHP patch level in the image build rather than relying on a floating latest tag.

docker compose build --pull web
docker compose up -d --no-deps web
docker compose ps
docker compose exec web php --version

Use the deployment system's normal immutable rollout process if the production platform is Kubernetes, Nomad or a managed container service. The important verification point is that new pods or tasks are actually running the rebuilt image and that old vulnerable instances have drained.

Update Drupal and contributed extensions separately

The PHP incident does not remove the need to address Drupal's October security advisories. On 14 October 2026, Drupal published a critical core advisory for a remote-code-execution issue in the Field UI module. The advisory stated that core alone was not sufficient for direct exploitation in every scenario, but that serious exploit paths existed with contributed modules and custom code. ([drupal.org](https://www.drupal.org/security/release-window?utm_source=openai))

Use the project's normal Composer workflow from the repository root. First create a deployable backup and inspect the working tree:

git status --short
composer validate --strict
vendor/bin/drush status

For a Composer-managed Drupal installation using the recommended core package, update the core metapackage with its dependencies:

composer update drupal/core-recommended drupal/core-composer-scaffold drupal/core-project-message --with-all-dependencies

If the project uses a different Composer template, follow that template's documented package set. Do not remove the lock file or run an unrestricted dependency update merely to make the security change “work”; that creates an unnecessarily large change set during an emergency.

Review the resulting changes, run the project's automated tests, and then deploy:

git diff --stat
git diff -- composer.json composer.lock

vendor/bin/drush updatedb:status
vendor/bin/drush cache:rebuild

Run database updates during the controlled deployment step if the update requires them. Take a database backup first, and ensure that the deployment process can be rolled back at the code and database layers. A rollback plan that restores only the previous files may leave an incompatible database schema.

Contributed projects also need individual review. The Drupal security advisory list recorded multiple October 2026 issues, including vulnerabilities in modules handling media, image derivatives, calendar views, authentication protection and file-related functionality. The fact that a module is not part of the current incident does not make it safe to ignore a newly published advisory. ([drupal.org](https://www.drupal.org/security/release-window))

Temporary mitigations when immediate patching is impossible

PHP documents several measures that reduce exposure to particular attack paths. They are useful for buying time during a maintenance window, but the project explicitly says that updating is the only complete solution. ([php.net](https://www.php.net/index.php))

For sites that do not need large uploads, reduce the accepted POST body size. Where uploads are required, set a limit appropriate to the application and keep upload_max_filesize below post_max_size:

post_max_size = 32M
upload_max_filesize = 24M

These settings must be applied in configuration loaded before request parsing. They should complement, not replace, request-size controls at the reverse proxy or web server. Check the effect on legitimate Drupal media uploads, content imports and administrative workflows before enforcing a production-wide limit.

PHP also recommends disabling session upload progress for the affected path:

session.upload_progress.enabled = 0

Restart every PHP worker after changing this value. An ini_set() call inside Drupal is too late for the relevant request-processing stage, and a per-user .user.ini file may not be loaded early enough. The same principle applies to other PHP settings intended to constrain request parsing: place them in the system configuration, FPM pool configuration or server-level configuration that is active before the request is handled.

Do not claim that these changes make a site safe. They are targeted reductions in exposure, not a patch. Avoid relying on generic WAF rules unless the vendor has documented exactly which requests they block and how the rule behaves for chunked or malformed requests.

Verify the deployment instead of trusting the change ticket

A patch is complete only when the runtime serving traffic has changed. Include these checks in the deployment record:

  1. Capture the PHP version before the change.
  2. Update the package, image or managed runtime.
  3. Restart PHP-FPM, Apache, workers and relevant containers.
  4. Confirm the new version through the real web path.
  5. Confirm that all load-balancer members report the patched version.
  6. Run Drupal smoke tests for anonymous and authenticated traffic.
  7. Review PHP and web-server logs for startup failures and abnormal requests.
  8. Remove temporary diagnostic files and emergency configuration that is no longer required.

For a Drupal smoke test, cover the functions most likely to expose a broken PHP or dependency deployment:

  • Front page and representative cached pages.
  • User login and logout.
  • Administrative login with an ordinary editor account.
  • Media upload and image derivative generation.
  • Content creation and revision workflows.
  • Cron and queue processing.
  • Search, email delivery and external API integrations.

Also inspect the deployment artifact itself. If the server is Composer-managed, record the lock-file hash or release identifier. If it is containerised, record the image digest rather than only the image tag. This makes it possible to prove which code and runtime were active during a later review.

Look for indicators of compromise without over-interpreting noise

The existence of a critical vulnerability does not by itself prove that a particular Drupal site was compromised. Conversely, a clean Drupal watchdog log does not prove that the host was untouched, because a runtime-level attack may occur before Drupal initialises.

Begin with a time-bounded review around the period before patching:

sudo journalctl -u php8.4-fpm \
  --since '2026-10-17 00:00:00' \
  --until '2026-10-22 23:59:59' \
  --no-pager

sudo find web/sites/default/files -type f -newermt '2026-10-17' -print

sudo find . -path './vendor' -prune -o \
  -type f -newermt '2026-10-17' -print

Adapt the dates to the actual exposure window and timezone. Investigate unexpected PHP files in upload directories, modified Drupal settings, new administrator accounts, unfamiliar cron entries, altered SSH keys, unknown processes and outbound connections. Treat file timestamps as clues, not proof: an attacker can alter timestamps, and legitimate deployments can create many files.

Review web-server access logs for unusual POST requests, repeated malformed requests, unexpected requests to upload directories and traffic followed by process crashes or worker restarts. Preserve the relevant logs before rotation, and involve the hosting provider if the site runs on managed infrastructure.

If there are credible indicators of compromise, isolate the affected workload according to the organisation's incident-response plan. Do not simply delete suspicious files and declare the incident resolved. Capture a forensic copy where appropriate, rotate credentials that may have been exposed, review database and third-party service access, and rebuild from a known-good source rather than trusting an in-place cleanup.

Governance improvements after the emergency

This incident is a useful test of Drupal operational maturity. A site that can patch Drupal but cannot identify the PHP-FPM service, rebuild its production image or verify every web node has a runtime-visibility problem.

Make the following controls routine:

  • Maintain an inventory linking each Drupal site to its PHP execution mode, version, host and owner.
  • Monitor PHP security releases separately from Drupal module updates.
  • Use supported PHP branches and schedule migrations before end-of-life dates.
  • Pin production image digests and retain build provenance.
  • Test emergency updates in a production-like environment without delaying a critical patch indefinitely.
  • Automate post-deployment checks through the real load-balanced route.
  • Keep centralised logs long enough to investigate security advisories retrospectively.
  • Document which mitigations are emergency-only and remove them after patching.

For Composer projects, a dependable update pipeline should fail when the lock file is stale, run Drupal's test suite, build the exact release artifact and publish a version identifier. For infrastructure, configuration management should make PHP package versions, FPM services and restart behaviour explicit instead of leaving them to manual memory.

What Drupal teams should do now

If you manage Drupal sites that were exposed to the internet on or before 21 October 2026, treat the PHP update as an infrastructure emergency rather than an ordinary maintenance task. Identify the web-serving runtime, patch it through the supported package or image channel, restart every relevant worker and verify the version through real traffic.

Then address the Drupal core and contributed-project advisories independently, test the application, review logs and preserve evidence of what changed. Temporary PHP settings can reduce exposure while a change window is arranged, but they are not substitutes for the fixed runtime releases.

The practical lesson is straightforward: Drupal security depends on the complete execution stack. Application dependencies, PHP binaries, service processes, containers, reverse proxies and operational verification all belong in the same patching conversation.