On 20 October 2026, two serious flaws in the PHP core were made public. The common name is PHP2Shell and the identifiers are CVE-2026-109618 and CVE-2026-109619. The CVSS score is 9.2. They allow code to run on the server without authentication, and attacks have already been seen.

In this article I explain what they are and which PHP versions are involved. I describe how I secured the servers I manage while there was no patch, and how I got to the final update. You will also find the script I used and the commands to run it.
What PHP2Shell is
The problem lies in how PHP reads multipart/form-data requests. This is the format the browser uses to send files. PHP parses it before any script starts.
So you do not need an upload form. A single request to any PHP file on the site may be enough.
The two flaws, in short:
- CVE-2026-109618. If memory runs out while PHP is reading the request, some data gets corrupted. The corruption shows up in the final clean-up of the request and can lead to code execution.
- CVE-2026-109619. Part of a request’s state stays in memory. A later request on the same process reuses it.
These technical details come from the analysis by the Dutch NCSC and from other public sources. I have not tried to exploit the flaws on my own servers.
Which PHP versions are affected
The fixed versions came out on 21 October 2026: PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. Every version before these is affected.
Branches 8.1 and older have no official fixed release, because they are out of support. PHP has only published minimal source backports for 7.4, 8.0 and 8.1. For even older branches, nothing has appeared.
The risk covers every way of running PHP. The most exposed is mod_php. PHP-FPM, php-cgi, FrankenPHP and LiteSpeed count too. It also affects every application that runs on PHP: WordPress, Joomla, Drupal, Nextcloud, phpMyAdmin, Roundcube and the ISPConfig panel.
On Debian there is a detail worth knowing. Debian applies fixes as backports and does not change the PHP version number. A server can show a low number and already be fixed. That is why the script does not compare versions. It checks whether the changelog of the installed package mentions the two CVEs.
The mitigation, while waiting for the patch
When the flaws became public, the patches were not yet available for the distributions. I did not want to leave the servers exposed, so I reduced the risk with a script.
The mitigation is not a patch. Turning off session.upload_progress closes only one of the known paths for CVE-2026-109619 and does nothing against CVE-2026-109618. It was meant to narrow the attack surface until the update, not to replace it.
What the script does
The script is designed for Debian 12 and 13. It works with Apache (mod_php, FastCGI, suPHP, PHP-FPM) and with Nginx (PHP-FPM). It recognises servers managed with ISPConfig, both Apache and Nginx.
The options:
- With no options it only checks. For each PHP package it verifies that the changelog mentions the two CVEs. It flags processes that still use an old binary, and reads the active settings and the listening web ports.
--updaterefreshes the package index first.--logslooks for suspicious signs: recent PHP files in upload or cache folders, typical webshell patterns, references to php2shell in the logs. It is a heuristic and does not prove that the system has been compromised.--hardenapplies the hardening described below.--dry-run, together with--harden, shows what it would do without changing anything.--skip-disable-functions, together with--harden, leavesdisable_functionsalone.--rollbackremoves what--hardenapplied.
What –harden changes
For each PHP version found in /etc/php, the script creates the file mods-available/cve-hardening.ini. It enables it with phpenmod for the fpm, apache2 and cgi SAPIs. This way the protection applies to all sites at once.
The original php.ini files stay untouched. The CLI is not touched either, so composer, artisan and wp-cli keep working.
The file sets three things:
session.upload_progress.enabled = Offexpose_php = Offdisable_functionswith exec, passthru, shell_exec, system, proc_open, popen and pcntl_exec, added to those already present.
Then the script tests the configuration and reloads PHP-FPM and Apache only if the test passes. Before writing anything, it makes a backup in /var/backups/php-cve-hardening/.
The limits to know about
disable_functionscan break some applications, for example WordPress with certain plugins, or Magento. If that happens, use--rollbackor run it again with--skip-disable-functions.- The
php_admin_valuedirectives in PHP-FPM pools take precedence over the global file. The script flags them but does not change them. - With ISPConfig, the files in
pool.dare regenerated by the panel. Overrides must be changed from the panel (Sites > Website > Options > Custom php.ini settings), not by hand. - Per-site
php.inifiles (ISPConfig FastCGI or suPHP mode) and PHP versions in/optare not handled. They need to be checked separately.
How to run the script
The script is here: https://cisilino.com/mscripts/php20261.sh
Before running anything from the internet, read it. You can download it and open it:
wget -qO php20261.sh https://cisilino.com/mscripts/php20261.sh
less php20261.sh
If you want to run it directly from the remote source, you need root privileges. These are the commands, in the order I recommend.
1. Check only, with a refresh of the package index:
wget -qO- https://cisilino.com/mscripts/php20261.sh | sudo bash -s -- --update
2. Hardening simulation, with no changes:
wget -qO- https://cisilino.com/mscripts/php20261.sh | sudo bash -s -- --harden --dry-run
3. Applying it:
wget -qO- https://cisilino.com/mscripts/php20261.sh | sudo bash -s -- --harden
4. Removal, if something breaks:
wget -qO- https://cisilino.com/mscripts/php20261.sh | sudo bash -s -- --rollback
After --harden and after --rollback you do not need to restart anything by hand: the script reloads PHP-FPM and Apache itself, after testing the configuration. A restart is needed, however, after updating the PHP package, because the running processes keep the old binary in memory:
sudo systemctl restart 'php*-fpm' apache2
The quotes around php*-fpm are needed. Without them, the shell expands the asterisk, not systemctl. If the server uses only Nginx, drop apache2.
The script also reports its outcome as an exit code: 0 if all is well, 1 if there are warnings, 2 if the system is vulnerable or action is needed.
On ISPConfig servers: ispconfig-multipart-guard
If you use ISPConfig, you have an extra tool. The project has released the ispconfig-multipart-guard script, described in the official ISPConfig advisory.
The principle is simple. If the problem is in multipart requests, the web server stops them before they reach PHP:
- it blocks
Content-Type: multipart/...requests for all sites, for some chosen sites or for the control panel; - with Nginx it answers 415 to PHP requests on blocked sites, and static files do not change. With Apache it answers 403, static files included;
- by default it blocks only the ISPConfig panel. Sites must be blocked with commands such as
block allorblock esempio.it; - it creates a cron job that restores its hooks every 5 minutes, in case ISPConfig rewrites the configuration;
- it has a list of authorised IPs that can still upload files.
It works with Debian and Ubuntu, with ISPConfig 3.2 or 3.3 (and also without ISPConfig), with Nginx or with Apache 2.4 and later.
The price is that uploads do not work on blocked sites. That includes CMS media, form attachments, webmail attachments and phpMyAdmin imports. Normal pages and urlencoded forms keep working. This too is a temporary measure and not a fix.
This script and mine are not alternatives. Mine acts on the PHP configuration, the ISPConfig one on the web server. I have not compared them in the field and I cannot tell you how they behave together on every setup.
The final update, in two steps
The mitigations were meant to last a short time. I updated the servers as soon as the updates were available, in two steps:
- Direct build. While waiting for the distribution used on each server to release its package, I built the fixed PHP versions from source.
- Official package. When the distribution’s official package was released for that server, I replaced the build with the package.
The second step matters. A hand-built PHP does not update itself with the rest of the system. The distribution package does, and the script can verify it by reading the changelog.
What to do now
- Update PHP to 8.5.12, 8.4.27, 8.3.36 or 8.2.35. On Debian you can also use the distribution package when the changelog mentions the two CVEs.
- Restart PHP-FPM and Apache, then run the script again with no options. It should no longer report problems.
- If you use PHP 8.1 or earlier, plan a move to a supported branch. There is no official fixed version for those branches.
- Keep
session.upload_progressturned off until the next round of fixes, if your application does not use it. - Check the logs and recent files, with
--logsor by hand, looking for PHP files that have appeared in upload folders. - Do not forget PHP inside containers, images and third-party appliances. They need updating separately.
- If your site uses WordPress, the Cisilino Site Defense Network plugin does not solve PHP2Shell. It does protect the login and keeps an activity log, which helps you understand what is happening on the site.
Sources
- ISPConfig: PHP2Shell, critical PHP vulnerabilities (CVE-2026-109618, CVE-2026-109619)
- NCSC-NL: PHP2Shell wiki
ISPConfig, PHP, Debian and the other names mentioned are trademarks of their respective owners. They are used only to identify the products.

Leave a Reply