October 22, 2026
How to Check if Your PHP Project Is Exposed (or Already Compromised) by PHP2Shell
A practical, defensive checklist using static analysis, server forensics and a small Rust CLI.

By Hebert Barros
5 min read
On October 21, 2026, researchers from Wiz, OpenAI Codex Security and Calif disclosed PHP2Shell: two critical flaws in the PHP runtime itself, tracked as CVE-2026โ109618 and CVE-2026โ109619 (severity up to CVSS 9.2). Together they can allow unauthenticated remote code execution or disclosure of process memory. Cloudflare reported attack traffic even before the official release date, because details leaked early.
The key point: the bug is in PHP, not in your code. A perfectly written Laravel app, a patched WordPress plugin or a plain "Hello World" can all be exposed if the runtime underneath is vulnerable.
This post answers two separate questions, because they are often confused:
- Am I exposed? (Is a vulnerable PHP version running?)
- Was I compromised? (Is there evidence someone already got in?)
A static scanner can answer the first. Only forensics can answer the second.
What PHP2Shell is, in one minute
Both CVEs are memory-safety problems that appear when PHP handles an error mid-request:
- CVE-2026โ109618: PHP runs out of memory before some internal data is initialized, then uses that unfinished data during cleanup.
- CVE-2026โ109619: a fatal error leaves stale references to memory that is freed at the end of the request. A later request in the same worker can follow them (use-after-free), leaking memory or running attacker-controlled code.
Because memory disclosure can steal passwords, API keys, tokens and certificates without ever running code, some attacks may leave very little trace. Keep that in mind for the second half of this post.
Step 1: Find out which PHP version you really run
Don't trust composer.json. It only says what your code allows, not what the server runs. Check the runtime directly, on every host and container:
php -v
php-fpm -v # or php-fpm8.3 -v on Debian/Ubuntu
apache2ctl -M | grep php # mod_php, if used
docker exec <container> php -vphp -v
php-fpm -v # or php-fpm8.3 -v on Debian/Ubuntu
apache2ctl -M | grep php # mod_php, if used
docker exec <container> php -vCompare against the first fixed releases published by the research team:
PHP branch
First fixed release
8.2
8.2.35
8.3
8.3.36
8.4
8.4.27
8.5
8.5.12
If you run 8.1 or older, the sources I reviewed list no fixed release for those branches. Treat that as "unverified, plan an upgrade", not as "safe". Check the official PHP advisory for the latest status.
Watch out for distro packages: Debian, Ubuntu and RHEL often backport fixes without changing the upstream version number, so confirm with your vendor's security tracker.
Step 2: Scan your repositories with php2shell_checker
For many projects (and many repos at once) it helps to check version declarations automatically. php2shell_checker is an open-source Rust CLI that does static analysis only. According to its README, it never executes scanned PHP code and makes no network requests during a normal scan.
Install from source:
git clone https://github.com/hebertcisco/php2shell_checker.git
cd php2shell_checker
cargo install --path .git clone https://github.com/hebertcisco/php2shell_checker.git
cd php2shell_checker
cargo install --path .Run a scan:
php2shell_checker scan ./my-project
php2shell_checker scan ./my-project --format json
php2shell_checker scan ./my-project --advisories ./advisories/custom.tomlphp2shell_checker scan ./my-project
php2shell_checker scan ./my-project --format json
php2shell_checker scan ./my-project --advisories ./advisories/custom.tomlIt reports versions as exact, range, floating or unknown, and uses a bundled, data-driven advisory file (advisories/php2shell.toml) that you can override.
Exit codes make it CI-friendly: 0 no findings, 1 findings detected, 2 usage/config error, 3 internal error. A minimal GitHub Actions gate looks like this:
- name: PHP2Shell exposure check
run: |
cargo install --git https://github.com/hebertcisco/php2shell_checker
php2shell_checker scan .- name: PHP2Shell exposure check
run: |
cargo install --git https://github.com/hebertcisco/php2shell_checker
php2shell_checker scan .Be realistic about its limits
The project is in early development. Today it reads PHP versions from .php-version, .tool-versions, composer.json and composer.lock only. Docker, CI providers, framework fingerprinting, php.ini analysis and SARIF output are listed as planned. And like any static tool, it cannot confirm what is actually deployed. A repo can say 8.4.27 while production still runs 8.4.20.
Use it as a fast first filter, then verify the runtime (Step 1) on real servers.
Step 3: Patch first, then investigate
If you are exposed, upgrade PHP and restart Apache, PHP-FPM and any long-running workers (Octane, FrankenPHP worker mode, RoadRunner, Swoole). Updating plugins or your CMS does not fix a runtime bug.
If you cannot upgrade immediately, the advisory suggests partial mitigations (for example disabling the finite memory_limit with an OS-level cgroup limit instead, reducing error display, limiting upload sizes). These are stopgaps only. They don't cover every variant, and they can hurt stability. One specific warning: don't use RLIMIT_AS or RLIMIT_DATA as replacement memory limits, since they can trigger the same vulnerable paths.
A WAF helps too. Cloudflare enabled default protections on all plans, including Free, on October 21, but even Cloudflare says patching is still required.
Step 4: Hunt for signs of compromise
No tool can prove you are clean. What you can do is look for the usual footprints of a PHP server takeover. Run these on the server (or a forensic copy), ideally as a read-only investigation.
4.1 Recently changed or suspicious files
PHP files modified since the disclosure window (adjust the date to your exposure period):
find /var/www -type f -name "*.php" -newermt "2026-10-01" -lsfind /var/www -type f -name "*.php" -newermt "2026-10-01" -lsPHP files where they should not exist (uploads, caches, images):
find /var/www -path "*/uploads/*" -name "*.php" -ls
find /var/www -path "*/storage/*" -name "*.php" -lsfind /var/www -path "*/uploads/*" -name "*.php" -ls
find /var/www -path "*/storage/*" -name "*.php" -lsCommon webshell markers (expect some false positives in legitimate code, so review each hit):
grep -rnE "eval\s*\(\s*(base64_decode|gzinflate|str_rot13)" /var/www --include=*.php
grep -rnE "(shell_exec|passthru|proc_open|popen)\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)" /var/www --include=*.phpgrep -rnE "eval\s*\(\s*(base64_decode|gzinflate|str_rot13)" /var/www --include=*.php
grep -rnE "(shell_exec|passthru|proc_open|popen)\s*\(\s*\$_(GET|POST|REQUEST|COOKIE)" /var/www --include=*.php4.2 Verify integrity against known-good sources
- Git-based deploys: run
git statusandgit diffon the deployed tree, or compare a hash of the deployed files with a fresh build. - WordPress:
wp core verify-checksumsandwp plugin verify-checksums --all. - Composer projects: reinstall from
composer.lockinto a clean directory and diff againstvendor/.
4.3 Processes and network activity
The PHP worker user (www-data, nginx, apache) should almost never spawn shells or make odd outbound connections:
ps -eo user,pid,ppid,cmd --forest | grep -Ei "php-fpm|apache|httpd" -A3
ss -tnp | grep -Ei "php|www-data"
crontab -l -u www-data
ls -la /etc/cron.* /var/spool/cron 2>/dev/nullps -eo user,pid,ppid,cmd --forest | grep -Ei "php-fpm|apache|httpd" -A3
ss -tnp | grep -Ei "php|www-data"
crontab -l -u www-data
ls -la /etc/cron.* /var/spool/cron 2>/dev/nullLook for shells (sh, bash), downloaders (curl, wget), unknown binaries in /tmp or /dev/shm, and new cron entries or systemd units.
4.4 Logs: crashes and memory pressure
The flaws involve memory exhaustion and fatal errors, so unusual spikes of these may deserve a look (this is a heuristic, not proof, because they also happen in normal incidents):
grep -i "Allowed memory size" /var/log/php*/*.log /var/log/php-fpm*/*.log
dmesg | grep -iE "segfault|oom" | grep -i php
grep -E " 50[0-9] " /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | headgrep -i "Allowed memory size" /var/log/php*/*.log /var/log/php-fpm*/*.log
dmesg | grep -iE "segfault|oom" | grep -i php
grep -E " 50[0-9] " /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | headAlso check web access logs for very large POST bodies or deeply nested form parameters from the same IPs, repeated just before errors.
4.5 Accounts and persistence
New admin users in your CMS, new SSH keys in ~/.ssh/authorized_keys, altered .htaccess or .user.ini files, and unexpected entries in auto_prepend_file or auto_append_file settings are all classic persistence tricks.
Step 5: Assume secrets may have leaked
Because CVE-2026โ109619 can disclose memory belonging to other requests, an attacker could have read secrets without leaving a webshell behind. If you ran a vulnerable version on an internet-facing server, the safe response is to rotate:
- database passwords and application keys (
APP_KEYin Laravel, salts and keys inwp-config.php) - API tokens, OAuth secrets and payment gateway keys
- session secrets (this will log users out, which is intended)
- TLS private keys if they were loaded by the PHP process
- cloud credentials available to the host or container
Then review access logs of the services those secrets protect for unusual activity.
Step 6: Keep it from happening again
- Pin PHP versions in Docker images (avoid
php:8orlatest) and rebuild images regularly. - Run a scanner like php2shell_checker in CI so outdated versions fail the pipeline.
- Monitor the PHP advisories and your distro's security feed.
- Keep a WAF on, as defense in depth.
- After patching, review
memory_limit,post_max_sizeandmax_input_nesting_leveltogether, and considerexpose_php = 0. - Run PHP workers with least privilege, restrictive filesystem permissions and egress filtering.
Quick decision guide
Situation
What to do
Patched version confirmed on the running server
Restart done? Then you're exposed no longer. Still do a quick log review for the window before patching.
Vulnerable version, internet-facing, unpatched for days
Patch now, then do Steps 4 and 5 fully.
Vulnerable version, internal only
Patch, rotate secrets as a precaution, review logs.
Version unknown (8.1 or older, floating tags)
Treat as unverified. Identify the real runtime, then upgrade.
Final thoughts
PHP2Shell is a reminder that the layer under your application matters as much as the application. A static scanner gives you fast visibility across many repositories, but the real answers come from checking the running runtime and the server's own evidence. When in doubt, patch, restart, rotate secrets and investigate.
Sources: Cloudflare's PHP2Shell blog post, php2shell.com (Wiz Research), and the php2shell_checker GitHub repository. Always confirm version details against the official PHP advisory at php.net.
Disclaimer: the commands above are general defensive checks, not a complete incident-response procedure. If you suspect a real compromise, preserve evidence and involve your security team or a professional responder.