PHP2Shell and Drupal: A Practical Post-Patch Audit Playbook

Tim Rabbetts | October 26, 2026

Emergency security releases often create a dangerous sense of completion. The package is updated, the service is restarted, the health check is green, and the incident appears to be over. For the two critical PHP vulnerabilities disclosed on 21 October 2026, that conclusion is too optimistic. Updating PHP closes the known vulnerability, but it cannot establish whether an attacker reached the server before the update.

This matters to Drupal teams because the vulnerable code runs in PHP before Drupal has finished bootstrapping. A request can therefore affect a Drupal site even when no vulnerable Drupal module is installed and no Drupal route appears in the attack path. The immediate priority is to run a patched PHP release. The second priority is to preserve evidence and perform a proportionate audit.

What happened in the PHP2Shell disclosure?

PHP published emergency fixes for CVE-2026-109618 and CVE-2026-109619 on 21 October 2026. Both vulnerabilities affect the PHP web runtime and can allow an unauthenticated remote caller to execute code, read data accessible to the PHP worker or cause denial of service. The fixed upstream versions are PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. ([aws.amazon.com](https://aws.amazon.com/security/security-bulletins/2026-143-aws/))

The Drupal Association’s response guidance is particularly relevant because it highlights the difference between patching and investigation. The vulnerabilities are in PHP rather than Drupal core, so there is no single Drupal-specific indicator that proves or disproves exploitation. The Association also reports that Amazon Threat Intelligence observed an exploitation attempt in its honeypot environment on 17 October, four days before public disclosure. ([drupal.org](https://www.drupal.org/association/blog/after-the-php-security-release-auditing-your-drupal-sites))

That does not mean every Drupal site was attacked, nor does a matching log entry prove compromise. It does mean that the useful audit window begins on 17 October 2026, not on the day your hosting provider applied the update.

First priority: prove that the runtime is patched

Do not rely on the PHP version shown by a local shell if the site is served by PHP-FPM, a container, a platform service or a separate pool. The PHP binary used by the command line can differ from the PHP runtime handling web requests.

Start with the command-line version:

php --version
php --ini

Then identify the active PHP-FPM service and inspect its version. The exact service name depends on the operating system and PHP package:

systemctl list-units --type=service --all | grep -E 'php.*fpm'
systemctl status php8.4-fpm

Replace php8.4-fpm with the service name used by the host. On a containerised deployment, inspect the running image rather than only the image definition:

docker ps --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}'
docker exec -it <php-container> php --version

For a Drupal application, also check what the web server exposes through a controlled administrative mechanism. Avoid creating a publicly accessible phpinfo() file. If you already have an authenticated diagnostics endpoint, use it temporarily and remove or restrict it after verification.

The minimum acceptable result is a fixed PHP release or a distribution package that explicitly backports the fixes. PHP 8.1 and earlier are end-of-life upstream branches in this incident and do not have a complete upstream fix for both vulnerabilities. AWS advises operators of those versions to migrate to a supported branch immediately; interim controls are not equivalent to patching. ([aws.amazon.com](https://aws.amazon.com/security/security-bulletins/2026-143-aws/))

Restart every relevant worker

Installing a new package does not necessarily replace already-running PHP workers. Restart PHP-FPM, the relevant web server where PHP is embedded, and any application workers or containers that include PHP:

sudo systemctl restart php8.4-fpm
sudo systemctl reload nginx

Use a reload for the web server only when that matches your deployment procedure. The important point is to ensure that old PHP processes have exited. Confirm the service state after the restart and check the PHP-FPM log for startup errors.

sudo systemctl is-active php8.4-fpm
sudo journalctl -u php8.4-fpm --since '15 minutes ago' --no-pager

In managed hosting, ask the provider for four specific confirmations:

  • Which PHP runtime serves the Drupal virtual host?
  • Which patched package or runtime build is installed?
  • When were all PHP workers restarted?
  • Were logs retained from 17 October 2026 onwards?

Preserve evidence before changing the environment

If there is any realistic possibility of exploitation, preserve logs before rotating, rebuilding or redeploying the server. The Drupal Association recommends copying web server, PHP-FPM and system logs to a safe location before investigation. In container environments, a redeploy can destroy precisely the evidence you need. ([drupal.org](https://www.drupal.org/association/blog/after-the-php-security-release-auditing-your-drupal-sites))

Create a restricted evidence directory outside the document root:

sudo install -d -m 0700 /root/incident-evidence-2026-10-26

sudo cp -a /var/log/nginx /root/incident-evidence-2026-10-26/ 2>/dev/null || true
sudo cp -a /var/log/apache2 /root/incident-evidence-2026-10-26/ 2>/dev/null || true
sudo cp -a /var/log/php* /root/incident-evidence-2026-10-26/ 2>/dev/null || true

sudo journalctl --since '2026-10-17' --no-pager \
  > /root/incident-evidence-2026-10-26/system-journal.txt

Do not modify the original logs while collecting them. If your platform provides centralised logging, export the relevant time range and record the query, time zone and retention settings. Timestamps are often the source of confusion during incident review, especially when application logs use UTC and local server logs use British Summer Time or GMT.

Inspect HTTP logs for suspicious request patterns

The two vulnerabilities have different external characteristics. One attack path involves multipart form processing, while the other can present as unusually large or malformed requests followed by worker failures. These clues are useful for triage, but none is conclusive on its own.

Begin by locating POST, PUT and PATCH requests from the audit window:

zgrep -hE ' (POST|PUT|PATCH) ' /var/log/nginx/access.log* \
  | awk '$4 ~ /17\/Oct\/2026|18\/Oct\/2026|19\/Oct\/2026|20\/Oct\/2026|21\/Oct\/2026|22\/Oct\/2026|23\/Oct\/2026|24\/Oct\/2026|25\/Oct\/2026|26\/Oct\/2026/' \
  > /root/incident-evidence-2026-10-26/methods.log

Access-log formats vary, so treat this as a starting point rather than a universal parser. If the date format differs, adjust the expression or export the relevant period from your logging platform.

Next, group request methods by client address. In the common combined nginx format, the first field is the client address and the sixth field contains the quoted request line:

awk '$6 ~ /"POST|"PUT|"PATCH/ {print $1}' \
  /root/incident-evidence-2026-10-26/methods.log \
  | sort | uniq -c | sort -nr | head -50

Look for clusters rather than isolated requests:

  • Multipart POST requests to routes that normally receive no uploads.
  • Multipart requests to the front page, normal node routes or JSON:API endpoints.
  • PUT or PATCH requests carrying an unexpected multipart content type.
  • Repeated requests from one address followed by 500, 502 or 503 responses.
  • Large or oddly shaped requests immediately before PHP worker crashes.
  • Traffic from the six source addresses listed in Amazon’s published report.

The source addresses are only indicators. They belong to shared anonymisation services, and a match should trigger closer examination rather than automatically being classified as a successful attack. Conversely, the absence of a match does not demonstrate that the site was not targeted. ([drupal.org](https://www.drupal.org/association/blog/after-the-php-security-release-auditing-your-drupal-sites))

Search PHP and web-server error logs

Failed exploitation attempts can leave a more visible trail than successful ones. Search for memory exhaustion, fatal errors, timeouts and worker crashes around the relevant dates:

sudo grep -RniE \
  'Allowed memory size|Out of memory|Maximum execution time|SIGSEGV|SIGABRT|Segmentation fault|child .* exited' \
  /var/log/php* /var/log/nginx /var/log/apache2 \
  2>/dev/null \
  > /root/incident-evidence-2026-10-26/runtime-errors.txt

Interpret results in context. Drupal workloads can legitimately generate memory exhaustion during large imports, cache rebuilds or image processing. A single familiar error is not evidence of PHP2Shell exploitation. A new cluster at an unusual endpoint, from a single client, followed by worker instability is more significant.

Check the native PHP session directory

Drupal normally stores sessions in its configured session backend, often the database, but PHP’s upload-progress handling can run earlier than Drupal’s bootstrap. On affected configurations, that means the native PHP session directory may contain useful evidence even on a Drupal site. The usual location varies by distribution and is defined by session.save_path. ([drupal.org](https://www.drupal.org/association/blog/after-the-php-security-release-auditing-your-drupal-sites))

Find the configured path:

php -i | grep '^session.save_path'

Then inspect recently changed files without opening untrusted content in an editor that may execute or interpret it:

sudo find /var/lib/php/sessions -type f \
  -newermt '2026-10-17' \
  -printf '%TY-%Tm-%Td %TH:%TM:%TS %u %s %p\n' \
  2>/dev/null \
  | sort \
  > /root/incident-evidence-2026-10-26/session-files.txt

sudo grep -RnaE '<\?php|upload_progress_' /var/lib/php/sessions \
  2>/dev/null \
  > /root/incident-evidence-2026-10-26/session-content-matches.txt

A recently created file, an unusual session identifier or an upload-progress key is a lead, not proof. Preserve suspicious files with their metadata, calculate hashes and avoid deleting them during the initial investigation.

Audit the Drupal application layer

Once the host and runtime evidence is preserved, review Drupal itself. You are looking for changes that an attacker with application-level access might have made after obtaining code execution or credentials.

Start with configuration drift:

drush config:status

Compare the result with the expected deployment state. Pay particular attention to newly enabled modules, changed roles, unfamiliar text formats, modified mail settings, new webform handlers, altered trusted host settings and unexpected changes to file-system or private-file configuration.

Review recent Drupal watchdog errors:

drush watchdog:show --severity=Error

Use the appropriate environment and access controls when running Drush. Do not run production diagnostics against a shared database from an unmanaged workstation.

Review accounts and privileged roles

Inspect accounts created or changed since 17 October 2026. The exact query depends on your Drupal version and data model, so use Drupal’s administrative reports or a read-only database query approved for your environment. Look for:

  • New users with administrator or content-maintainer privileges.
  • Existing accounts whose email address, password-reset data or status changed unexpectedly.
  • New API credentials, OAuth clients or integration accounts.
  • Roles assigned to users who do not normally need them.
  • Unusual content-format permissions, particularly permissions allowing HTML or embedded code.

Review audit records from the identity provider as well as Drupal logs. If Drupal authentication is delegated to an external identity service, a compromised PHP worker may still have been able to read local configuration or invoke privileged application operations without creating a normal Drupal login event.

Review the document root and writable directories

PHP web shells and dropped scripts are often placed in writable directories. Drupal’s public files directory should not be treated as an executable application directory. Check files changed after the start of the audit window:

find . \
  -newermt '2026-10-17' \
  -type f \
  -not -path './vendor/*' \
  -not -path './node_modules/*' \
  -not -path './.git/*' \
  -printf '%TY-%Tm-%Td %TH:%TM %u %p\n' \
  | sort \
  | tail -200

Search writable Drupal file areas for PHP-like extensions:

find web/sites/default/files -type f \
  \( -iname '*.php' -o -iname '*.phtml' -o -iname '*.phar' \) \
  -print

The path may be sites/default/files rather than web/sites/default/files, depending on the project layout. A legitimate PHP file outside the normal codebase is unusual and deserves investigation. Do not conclude that a file is malicious solely from its extension: preserve it, compare it with the expected deployment artefact and examine ownership, timestamps and hashes.

Apply useful PHP hardening after patching

Configuration changes can reduce exposure, but they are not substitutes for the fixed PHP release. The PHP and Drupal guidance recommends keeping session.upload_progress.enabled = 0 when the feature is not required. Drupal does not normally need PHP session upload progress for ordinary file uploads. Also consider enabling strict session handling with session.use_strict_mode = 1. ([drupal.org](https://www.drupal.org/association/blog/after-the-php-security-release-auditing-your-drupal-sites))

Set these directives in a system-level PHP configuration or PHP-FPM pool, not through Drupal’s ini_set() calls:

session.upload_progress.enabled = 0
session.use_strict_mode = 1

After changing the configuration, restart PHP-FPM and verify the effective values:

sudo systemctl restart php8.4-fpm
php -i | grep -E '^session.upload_progress.enabled|^session.use_strict_mode'

For PHP-FPM deployments, verify the web-serving pool rather than assuming the command-line output represents the same configuration. A pool-level setting may override the CLI configuration, and a container may use a different php.ini altogether.

Review upload limits as part of normal attack-surface reduction. Keep post_max_size and upload_max_filesize aligned with the application’s actual requirements. Do not blindly copy emergency values from another environment: reducing upload limits can break legitimate media workflows, while setting them too high increases resource consumption.

Do not forget Composer dependencies

PHP runtime patching and Drupal dependency auditing solve different problems. Composer can audit packages installed by the project against security advisories, detect abandoned packages and report malware or policy findings. Run it from the project root:

composer audit --locked --format=summary

For machine-readable CI output:

composer audit --locked --format=json > composer-audit.json
test ! -s composer-audit.json

The second command is only a simple file check, not a reliable policy gate. Composer’s audit command returns exit code 0 when no issues are found and 1 when findings or required advisory data are present. Use the command’s exit status directly in CI and decide whether abandoned packages should fail the build. ([getcomposer.org](https://getcomposer.org/doc/03-cli.md))

Remember that Composer cannot detect every compromise. A malicious file added after deployment, a stolen environment secret or a compromised operating-system package will not necessarily appear in a dependency audit.

When should this become an incident?

Escalate from routine patch verification to formal incident response when several indicators cluster around the same time or client. Examples include a suspicious multipart request followed by PHP-FPM crashes, newly created executable files, unexplained administrator accounts, altered deployment files or evidence that secrets were read by the web-server user.

If compromise is plausible, isolate the host while preserving it in a running state where practical. The Drupal Association advises preserving logs and, where possible, a disk image before rebuilding. Rotate every secret the web-server user could have accessed, including database credentials, the Drupal hash salt, API keys, SMTP credentials and private keys. Treat other systems that share those secrets or a session store as potentially exposed. ([drupal.org](https://www.drupal.org/association/blog/after-the-php-security-release-auditing-your-drupal-sites))

Do not “clean” a suspected server by deleting a single suspicious file and continuing as normal. Rebuild from a known-good source, restore only validated data, rotate credentials and review the deployment pipeline. If the site processes personal data or operates under contractual security obligations, involve the organisation’s incident-response and data-protection contacts early.

Build this into the normal Drupal maintenance process

The long-term lesson is broader than this PHP disclosure. A Drupal maintenance process should inventory every PHP runtime, not just the version declared in composer.json. It should know where PHP-FPM runs, which services restart after a package update, how long logs are retained and whether writable directories are monitored.

A practical baseline includes:

  • Automated PHP version checks for every production web and worker environment.
  • Composer audits in CI and before production deployments.
  • Centralised access, PHP-FPM and system logging with a defined retention period.
  • File-integrity monitoring for the Drupal document root and writable directories.
  • Regular reviews of privileged Drupal users and configuration drift.
  • Documented emergency patch procedures, including rollback and evidence preservation.
  • Secret rotation procedures that do not depend on rebuilding the entire platform manually.

Web application firewalls and managed rules can provide useful defence in depth, especially while a fleet is being patched, but they cannot inspect every runtime-level condition. AWS explicitly describes WAF protection as a compensating control and states that upgrading to a fixed PHP release is the only complete fix. ([aws.amazon.com](https://aws.amazon.com/blogs/security/using-aws-waf-to-apply-mitigations-for-php2shell-rce-vulnerabilities-cve-2026-109618-and-cve-2026-109619/?utm_source=openai))

Conclusion

For Drupal sites affected by the October 2026 PHP emergency, the correct sequence is straightforward but not merely “run updates”. First, prove that every PHP worker is running a fixed release. Second, restart the workers and preserve the evidence that existed before the restart. Third, audit HTTP traffic, runtime errors, native PHP session storage, Drupal configuration, privileged accounts and writable directories from 17 October 2026 onwards.

Most sites will not find evidence of compromise. The value of a disciplined audit is that it turns that conclusion from an assumption into a defensible result. Patching removes the vulnerable condition; evidence-led review tells you what happened before the patch and what governance action should follow.