Article
PHP2Shell: what it is, how to tell if you were hit, and what to do now
Two critical PHP flaws can let an attacker run code on a server without logging in, and exploitation was attempted before the fix shipped. How to check and patch every Laravel, Statamic and Craft server, and look for signs you were hit.
Porro team · · 11 min read
On 20 October, PHP warned users to get ready for an emergency security release after exploit details started circulating ahead of the planned disclosure. PHP published the advisories and the fixed releases over the following two days. The two flaws, CVE-2026-109618 and CVE-2026-109619, are known together as PHP2Shell, and both are rated 9.2 out of 10.
On a vulnerable and exploitable set-up, an attacker with no account may be able to run their own code on the server as the PHP worker's user. How easy that is depends on how PHP runs, its configuration, the application and the exact build. Exploitation attempts have been seen in the wild, including one Amazon recorded on 17 October, days before the fixes, and CISA has since added both CVEs to its Known Exploited Vulnerabilities catalogue. Public reporting doesn't say how many servers were actually compromised.
The flaws are in PHP itself, not in Laravel, Statamic, Craft or a package you can update with Composer. So every site served by a vulnerable PHP runtime should be treated as at risk. If you look after client sites, here's what to do, in order.
The short version
- Update PHP to 8.2.35, 8.3.36, 8.4.27 or 8.5.12, or your distribution's patched package, then restart every PHP service so old workers are replaced.
- If a server can't be updated today, restrict access to it and apply PHP's temporary mitigations below. PHP is clear that none of them is complete.
- Check whether you were hit before you patched. Patching closes the door; it doesn't remove anyone who already got in.
- On PHP 8.1 or older, use your distribution's patched package or upgrade. PHP's own releases only cover 8.2 and newer.
What PHP2Shell is
Both flaws are memory-safety bugs in how PHP handles requests:
- CVE-2026-109618 is an out-of-memory corruption flaw. When PHP hits its memory limit, the operation is abandoned while some request data is only partly set up, and cleanup code can then treat that broken state as valid.
- CVE-2026-109619 is a use-after-free caused by stale global state. One request leaves pointers to its data behind after that memory is freed, and a later request handled by the same worker uses them before they're reset. One confirmed route runs through session upload progress, a feature that's on by default.
PHP rates Apache's mod_php as the highest risk, because an exploit was shown there that works before any application code runs. PHP-FPM, the default on Laravel Forge, Ploi and most VPS set-ups, is affected too, but its demonstrated path needs a suitable operation in the application, a fatal error, and a later request landing on the same worker. Either way, every web-serving PHP install needs the update.
Are you affected?
| PHP branch | Fixed in | Notes |
|---|---|---|
| 8.5 | 8.5.12 | |
| 8.4 | 8.4.27 | |
| 8.3 | 8.3.36 | |
| 8.2 | 8.2.35 | |
| 8.1, 8.0, 7.4 | No new upstream release | Unsupported branches. PHP published minimal source patches for vendors and people who build PHP themselves. Use a vendor-patched package, or upgrade to 8.2 or newer. |
| 7.3 and older | No fix | Upgrade. |
Two things catch people out.
Check the PHP that serves the site, not the one in your terminal. php -v reports the command-line PHP, which can differ from PHP-FPM. On Ubuntu, check the FPM binary, the installed package and the running service:
/usr/sbin/php-fpm8.3 -v apt-cache policy php8.3-fpm sudo systemctl status php8.3-fpm --no-pager
Distributions keep the upstream version and change the package revision. Ubuntu, Debian and Red Hat often ship fixes as a new revision of, say, PHP 8.3.6, so the PHP version alone doesn't tell you whether you're patched. Compare the full package version with your vendor's security advisory:
dpkg-query -W -f='${Package} ${Version}\n' php8.3-fpm php8.3-common
On a managed platform such as Laravel Cloud or Vapor, the provider manages the PHP runtime. Look for their confirmation that the fixed versions are rolled out rather than assuming.
Fix it now
On a server you manage, such as Forge, Ploi or your own, check that your repository offers a fixed build, update, and restart. On Ubuntu with the usual PPA, for PHP 8.3:
sudo apt update apt-cache policy php8.3-fpm php8.3-cli php8.3-common sudo apt-get install --only-upgrade php8.3-fpm php8.3-cli php8.3-common sudo systemctl restart php8.3-fpm
List the rest of your installed PHP packages with dpkg-query -W 'php8.3-*' and upgrade those too. Repeat for each PHP version on the server. If you use Apache with mod_php, restart Apache as well. The restart matters, because running workers keep the old code in memory until they're replaced.
If you can't update today
PHP's advice is that there's no complete mitigation, and updating is the only safe choice. If a server genuinely can't be updated today, first restrict access to it: put it behind a VPN or an IP allow-list, or take it offline, as the UK's NCSC suggests for systems that can't be fixed in time. These temporary measures from PHP's advisories then reduce the risk. Test each one, because some can break uploads or hurt performance.
-
Turn off session upload progress, administratively. In the PHP-FPM pool config, add
php_admin_flag[session.upload_progress.enabled] = off. With Apache'smod_php, putphp_admin_flag session.upload_progress.enabled Offin the server, virtual host or<Directory>config, not in.htaccess.ini_set()and.user.inidon't work for this. Then check the config and restart:sudo /usr/sbin/php-fpm8.3 -t && sudo systemctl restart php8.3-fpm sudo apache2ctl configtest && sudo systemctl restart apache2
Laravel, Statamic and Craft don't use upload progress themselves, but check your own code before turning it off.
-
Lower
post_max_size, administratively. For CVE-2026-109618, PHP suggests8Mwhere the site takes no uploads, or32Mwhere it does. Keepupload_max_filesizebelow it. Test forms and asset uploads afterwards. -
memory_limit = -1, set administratively, is also in PHP's guidance for CVE-2026-109618. It stops attackers from triggering PHP's own memory-limit failure, but it isn't a complete fix, and on its own it lets one request use all the server's memory. Don't replace it withRLIMIT_ASorRLIMIT_DATAlimits: PHP warns those can reach the same vulnerable path. Read the advisory before using this. -
As a last resort on PHP-FPM,
pm.max_requests = 1stops a worker serving a second request, which breaks the demonstrated route for CVE-2026-109619. It doesn't help with CVE-2026-109618, and it can slow a busy site badly. Test it under load first.
The third flaw: CVE-2026-115560
On 23 October, PHP published an advisory about a further possible code-execution path, CVE-2026-115560, with no code fix released as of 29 October. It needs three things at once: the session serializer configured as php_binary or php_serialize, application code that switches it to php with ini_set() before session_start(), and upload progress left on. Applications that never change the serializer at runtime don't meet the known conditions. PHP's interim advice is the same setting: session.upload_progress.enabled = 0.
How to tell if you were already hit
Exploitation was attempted before the fix existed, so check every server that ran a vulnerable version on the internet, not just patch it. Contain first: restrict or patch the exposed server straight away. Then preserve logs and take a snapshot as soon as you can, without holding up the fix to get a perfect copy. Keep that copy locked down, because it contains live secrets and personal data.
Some researchers have published IP addresses, payload names and other indicators. They only cover campaigns that were seen, and attackers can change them easily, so use them to add context, not to declare a server clean.
Everything below gives you leads, not proof. Know what a normal deploy looks like first, look for several signals that line up, and keep suspicious files rather than deleting them. The paths assume a Forge-style set-up (/home/forge/example.com, user forge); adjust them for yours.
1. PHP files you didn't deploy. Laravel, Statamic and Craft send every request through one index.php, so other PHP files in the public folder deserve a look:
find /home/forge/example.com/public -xdev -type f -iname '*.php' ! -name index.php -print
Then list every PHP file changed in storage or public since the first reported attempts, including compiled views. They're a good hiding place precisely because PHP files are expected there:
sudo find /home/forge/example.com/storage /home/forge/example.com/public \ -xdev -type f -iname '*.php' -newermt '2026-10-16 23:59:59' \ -printf '%TY-%Tm-%Td %TH:%TM:%TS %u %m %p\n'
Timestamps can be faked, so compare anything odd with a trusted deploy or backup.
2. Changes to tracked files. If the site deploys from Git, run these as the deploy user:
sudo -u forge git -C /home/forge/example.com status --porcelain=v1 sudo -u forge git -C /home/forge/example.com diff --stat sudo -u forge git -C /home/forge/example.com diff --cached --stat
Look at untracked files separately. Git doesn't cover ignored files, .env, storage, or anything outside the repository.
3. Worker crashes and memory errors. Attempts tend to crash PHP workers or hit the memory limit. Ordinary bugs and traffic spikes do too, so treat these as things to line up with other signals:
sudo journalctl -u php8.3-fpm --since '2026-10-17' | grep -Ei 'SIGSEGV|signal 11|exited on signal|Allowed memory size|out of memory' sudo zgrep -Ehi 'SIGSEGV|signal 11|exited on signal|Allowed memory size|out of memory' /var/log/php8.3-fpm.log* 2>/dev/null sudo find /home/forge/example.com/storage/logs -type f -name 'laravel*.log' -exec grep -EH 'Allowed memory size|out of memory' {} +
4. Leftover session data. Public exploits write attacker-supplied data into a PHP session file, and an interrupted attempt can leave it behind. First find where PHP keeps sessions: check session.save_handler, session.save_path and session.upload_progress.prefix in your FPM pool and php.ini. If the handler is files, search that folder:
sudo grep -Il -- 'upload_progress_' /var/lib/php/sessions/sess_*
A match is a lead, and no match doesn't rule anything out. PHP's session settings are separate from Laravel's SESSION_DRIVER, so check both.
5. Persistence. Look for scheduled jobs, services and SSH keys you didn't add, and for unexpected connections:
sudo crontab -l -u forge; sudo crontab -l -u root sudo ls -la /etc/cron.d /var/spool/cron/crontabs sudo systemctl list-timers --all sudo cat /home/forge/.ssh/authorized_keys sudo ss -tpn
ss only shows connections open right now, so an empty result proves nothing.
6. Access logs. Look at requests to every PHP route, including the normal index.php. Bursts of multipart POST requests, 5xx errors that line up with worker crashes, and repeated requests from the same source are all worth a closer look. The byte count in a standard access log is the response size, not the request size, unless you've configured your server to log request length.
If you find something
- Contain it. Take the site offline or restrict access. Don't just delete the file you found: whoever left it may have left others.
- Assume every secret on the server is known. Rotate database passwords, API keys, mail and storage credentials, and everything else in
.env.- Laravel: rotate
APP_KEY, which logs everyone out. Laravel'sAPP_PREVIOUS_KEYSexists for planned rotations, but don't keep a key an attacker may have: it would let them forge encrypted cookies. Data you've encrypted yourself isn't re-encrypted automatically. - Craft: generate a new security key with
php craft setup/security-key. Anything encrypted with the old key, including session cookies, can no longer be read, so plan for everyone being logged out.
- Laravel: rotate
- Rebuild rather than clean. Provision a fresh server on a patched PHP, deploy from Git, and restore data from a backup taken before the compromise. Cleaning a server in place leaves you guessing.
- Tell the client straight away. If you host or look after the site for them, you're usually their processor, and you must tell them without undue delay. As controller, the client decides whether the breach is likely to put people at risk. If it is, they must report it to the ICO without undue delay, and within 72 hours of becoming aware where feasible. If the risk is high, they must tell the people affected too.
Keeping track across every client site
The hard part for an agency isn't one server. It's knowing which of forty client sites run which PHP build, and which are patched. Porro reads the PHP version from every site, shows which sites a security release like this one affects and which are patched, and on Forge can start the PHP update for you. Start a free trial to see your own sites.
Sources, checked 29 October 2026:
- GHSA-w8hw-w9fx-j79w: RCE via out-of-memory corruption (PHP)
- GHSA-6wmf-4rv5-p3rp: RCE via stale global state (PHP)
- PHP 8 changelog and release notice (PHP)
- PHP2Shell: Find, Mitigate, and Patch Now (Qualys)
- PHP2Shell RCE exploited (watchTowr)
- Understanding PHP2Shell (Akamai)
- CISA confirms PHP2Shell under attack (The Register)
- PHP warns of another unpatched RCE risk (The Hacker News)
- Personal data breaches: a guide (ICO)
Get the weekly digest
One short email on Fridays: the advisories and end-of-life dates that matter for Laravel, Statamic, Craft and JavaScript sites.
Fridays · unsubscribe in one click · stored in the UK