DEV Community

Cover image for Post-Patch Triage for PHP2Shell: A Practical Checklist for Laravel Servers and Docker
Harry Agustiana
Harry Agustiana

Posted on

Post-Patch Triage for PHP2Shell: A Practical Checklist for Laravel Servers and Docker

PHP2Shell (CVE-2026-109618 and CVE-2026-109619) is fixed in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. This post covers what comes after the update: a triage you can run on a Laravel server, with Docker notes. Commands assume Linux, and paths vary by stack, so adjust them. Everything below is read-only inspection, and none of it replaces a proper forensic process.

Use 2026-10-20 as your cutoff date. That is when the CVE identifiers started circulating, before the patch existed.

1. Confirm the version that serves web traffic

The CLI version is not necessarily the version behind your web server.

php -v
php-fpm -v            # or php-fpm8.2 -v on Debian/Ubuntu
apachectl -M | grep -i php
Enter fullscreen mode Exit fullscreen mode

If you installed from a distribution package, the version string can look old while the fix is present. Check the changelog for the CVE instead:

# Debian/Ubuntu
apt changelog php8.2-fpm | grep -i 'CVE-2026-1096'
# RHEL family
rpm -q --changelog php-fpm | grep -i 'CVE-2026-1096'
Enter fullscreen mode Exit fullscreen mode

Updating Laravel does not update PHP. Restart PHP-FPM, Apache and any long-running workers (queue workers, Octane) after the update.

2. Docker

Each container carries its own PHP. The host's PHP does not matter here.

docker exec <container> php -v
docker build --pull --no-cache -t myapp:2026-10-22 .
docker compose pull && docker compose up -d --build
Enter fullscreen mode Exit fullscreen mode

Watch for tags pinned to an old patch version (for example php:8.2.30-fpm). --pull will not move those. Edit the tag. Rebuild queue worker and scheduler containers too, not only the web one.

3. Files that should not be there

# PHP files modified since the cutoff, excluding dependency folders
find /var/www -type f -name '*.php' -newermt '2026-10-20' \
  -not -path '*/vendor/*' -not -path '*/node_modules/*'

# PHP-like files where none should live
find /var/www/app/storage /var/www/app/public/uploads -type f \
  \( -name '*.php' -o -name '*.phtml' -o -name '*.phar' \)

# If the deploy directory is a git checkout
git -C /var/www/app status --porcelain
Enter fullscreen mode Exit fullscreen mode

Legitimate deploys also change files, so compare against your deploy history before panicking.

4. Persistence

sudo ls -la /etc/cron.d /var/spool/cron/crontabs 2>/dev/null
systemctl list-timers --all
awk -F: '$3 >= 1000 {print $1}' /etc/passwd
sudo find /home /root -name authorized_keys -exec ls -l {} +
last -n 20
Enter fullscreen mode Exit fullscreen mode

You are looking for anything you cannot explain: a job, a timer, a user or a key nobody on your team added.

5. Logs

The researchers suggest watching for unusual multipart requests followed by memory limit errors, repeated HTTP 500 responses, oversized PHPSESSID response cookies and unexplained worker restarts.

grep -i 'allowed memory size' /var/log/php*-fpm.log | tail -50
awk '$9 == 500 {print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
Enter fullscreen mode Exit fullscreen mode

The status field position assumes the common log format. Adjust if yours differs.

6. Rotate what a worker could have held

The flaws can expose worker memory, so assume anything the app handles could have been read. At minimum:

  • database passwords
  • third-party API keys and mail credentials
  • APP_KEY, with care: rotating it invalidates sessions and anything encrypted with it. Recent Laravel versions support APP_PREVIOUS_KEYS for graceful rotation, so check the docs for your version.
  • active sessions (clear the session table or files)

Then run php artisan config:clear and redeploy.

7. Where a scanner fits

A filesystem scanner is one useful input here. Laravel Scalpel (open source, my project) covers a defined slice: PHP and PHP-like extensions in non-PHP zones such as public and storage, common obfuscation patterns, dangerous .htaccess and .user.ini directives, .env integrity, and a baseline diff. I ran it against three Laravel applications and it found no backdoor files. Three practical notes for using any in-process scanner after an RCE:

  • Trigger it from outside the app's user. An in-app run uses the same OS user as PHP, so an attacker with code execution could tamper with it. I run it both ways: from inside the application, and through an external runner (the sentinel.sh pattern from n8n-bastion) on a schedule that posts findings to Telegram. I weigh the external result more. Send output somewhere the web user cannot write, and use scalpel:verify if you enable signing.
  • Make the baseline refresh the last step, not the first. scalpel:baseline --force bakes in whatever is on disk at that moment. I refreshed mine after the update, which is safe only because scheduled external diffs had been running before it, so any unexpected change would already have raised an alert. Without that, scan and diff first, and archive the previous snapshot (by default storage/app/private/scalpel/baseline.json) somewhere the web user cannot write. Enable output signing before creating the first baseline, so a regenerated baseline gets flagged.
  • Check that silence means something. No alert is evidence only if the scheduled runs executed during the window and the alert level covers the changes you care about. Confirm both in your runner's execution history. Alerts are filtered by severity (--fail-on, plus any CRITICAL or HIGH filter in the runner), so a low-severity change may not page you.
  • Treat vendor and cache paths deliberately. By default vendor/ and bootstrap/cache are skipped by the content scanners (the baseline diff still hashes them). After an incident, php artisan scalpel:scan --include-vendor is worth the extra time.

None of this covers cron, keys, logs or memory, which is exactly why steps 3 to 5 exist.

8. After the update, and if you cannot update yet

The fix adds a new hard memory limit, and hitting it ends the process without running shutdown handlers. Review memory_limit and post_max_size against what your app really needs.

Only updating is a complete fix. If you are stuck, these reduce exposure to specific paths and do not make the server safe:

  • CVE-2026-109618: set memory_limit=-1 so applications cannot override it, keep post_max_size low, and enforce request size limits at the web server or proxy. Do not use RLIMIT_AS or RLIMIT_DATA as a substitute, since they can trigger the same vulnerable path.
  • CVE-2026-109619: set session.upload_progress.enabled=0 and restart every worker. On PHP 8.5, also set fatal_error_backtraces=0.
  • A WAF rule is a stopgap. Cloudflare itself said attackers adapted payloads around its rules.

If you find real evidence of intrusion, rebuild from a clean image and a known good backup. Hand cleaning rarely catches everything.

Top comments (0)