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
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'
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
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
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
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
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 supportAPP_PREVIOUS_KEYSfor 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.shpattern 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 usescalpel:verifyif you enable signing. -
Make the baseline refresh the last step, not the first.
scalpel:baseline --forcebakes 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 defaultstorage/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/andbootstrap/cacheare skipped by the content scanners (the baseline diff still hashes them). After an incident,php artisan scalpel:scan --include-vendoris 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=-1so applications cannot override it, keeppost_max_sizelow, 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=0and restart every worker. On PHP 8.5, also setfatal_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)