PHP2Shell is the name for two critical PHP bugs, CVE-2026-109618 and CVE-2026-109619, that let an attacker run code on a web server with one crafted upload request, before any login and before your application's own code runs. PHP fixed them on 21 October 2026 at 14:00 UTC in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. Debian 13 had its fixed package out 15 minutes earlier: php8.4 8.4.27-1~deb13u1 in the trixie-security archive.
If you run PHP on Debian 13, one command patches every site on the server: apt update && apt upgrade. With automatic security updates switched on, the server does it by itself. We tested it: 200 PHP websites on one server the size of our VDS Small (4 dedicated vCPUs, 8 GB RAM) went from the vulnerable 8.4.24 to the fixed 8.4.27 in 12 seconds, with no failed request while PHP restarted. Below is what happened, why we put PHP sites on Debian 13, and the exact setup, including an emergency mode that checks for security fixes every hour.
Key facts, checked on 22 October 2026:
- Both bugs are rated CVSS v4.0 9.2. PHP's own advisory says the risk is highest for mod_php, where an unauthenticated request can trigger the attack before application code runs (php.net security release notes, GHSA-w8hw-w9fx-j79w, GHSA-6wmf-4rv5-p3rp).
- Debian published DSA-6574-1 for php8.4 on 21 October 2026 at 13:45 UTC. The Debian security tracker lists trixie as fixed in 8.4.27-1~deb13u1 and bookworm (Debian 12) in php8.2 8.2.35-1~deb12u1 through DLA-4848-1.
- Amazon saw a first exploitation attempt on its honeypots on 17 October, four days before disclosure, and Cloudflare reported attacks at 07:43 UTC on 21 October, before the fixes were out (Tenable FAQ, Sansec).
- One rough estimate quoted by Tenable puts more than 1.3 million internet-facing hosts at risk.
What happened: the PHP2Shell timeline
The bugs were found by several teams over the summer. Then the CVE numbers leaked a day early, so the PHP project moved its release forward by a day. All times are UTC and come from Sansec, Tenable, php.net and the Debian announcement linked above.
| When | What |
|---|---|
| 1 Aug 2026 | Calif finds several remote code execution paths in PHP's request handling. |
| 11 and 24 Aug 2026 | Calif, then Wiz Research, report the bugs to the PHP security team. |
| 17 Oct 2026 | Amazon's honeypot network logs the first exploitation attempt. |
| 20 Oct 2026 | Details leak. PHP posts an advance notice of a critical release. |
| 21 Oct, 07:43 | Cloudflare reports attacks in the wild. |
| 21 Oct, 07:53 | Debian's PHP maintainer finishes the fixed php8.4 package for trixie (the time on the package changelog). |
| 21 Oct, 13:45 | Debian publishes DSA-6574-1. The fix is in trixie-security. |
| 21 Oct, 14:00 | PHP releases 8.5.12, 8.4.27, 8.3.36 and 8.2.35, and the researchers publish their write-ups. |
What the bug does, in plain words
Every PHP request that sends a file upload uses a format called multipart/form-data, and PHP reads that body before your code starts. PHP2Shell abuses the moment PHP hits a fatal error during that phase, usually by running out of memory. PHP stops before it has cleaned up, and a later request on the same worker process treats freed memory as live data. Sansec says the vulnerable code goes back to PHP 5.4.0 and that the leak works even when the application never starts a session.
So a clean WordPress, Magento or Laravel install is still exposed. The bug sits in the PHP engine, not in your plugins. Both Apache's mod_php and PHP-FPM are affected, and default settings are vulnerable. Besides running code, the first bug can leak data from other visitors' requests: passwords, session cookies and API keys.
A firewall rule helps a little, but only updating PHP removes the problem. The temporary workarounds on php.net (turning off session.upload_progress.enabled, changing memory_limit and post_max_size) reduce the risk and can break uploads. PHP itself calls them partial.
Is my Debian server still vulnerable? A 30-second check
Log in over SSH and run this as root. It asks the package manager, not PHP, which version is installed, so it is right for PHP-FPM, Apache and the command line at the same time:
# run as root on the server
apt update
apt-cache policy php8.4-fpm | head -6
A patched Debian 13 server shows the fixed version and the security archive:
php8.4-fpm:
Installed: 8.4.27-1~deb13u1
Candidate: 8.4.27-1~deb13u1
Version table:
*** 8.4.27-1~deb13u1 500
500 http://deb.debian.org/debian-security trixie-security/main amd64 Packages
If Installed says 8.4.24-1~deb13u1 or anything lower than 8.4.27-1~deb13u1, the server is vulnerable. On Apache with mod_php, check libapache2-mod-php8.4 instead of php8.4-fpm. If Candidate also shows the old version, the server doesn't see the security archive. Make sure /etc/apt/sources.list (or a file in /etc/apt/sources.list.d/) has a line with trixie-security, then run apt update again. That was our lab's first state: a sources file with the security line missing. It is more common than people think on servers built from old templates.
Three setups need a different fix:
- PHP from the
packages.sury.orgrepository (used for newer PHP versions on Debian) gets its updates from that repository, not from Debian. The default automatic update settings only allow Debian's own archives, so those packages are not patched by themselves. Update them by hand, or add that repository's origin to the settings (shown below). - PHP in Docker gets fixed when you pull a new image and recreate the container. The official
phpimage has no fix for 8.1 or older. - PHP 7.4, 8.0 and 8.1 got no official release. The PHP team published source patches only, so plan the move to 8.4.
Why we put PHP applications on Debian 13
Debian 13 "trixie" was released on 9 August 2025, and its latest point release is 13.7 from 12 September 2026. It ships PHP 8.4. It gets full security support until 9 August 2028, and Debian LTS until 30 June 2030 (Debian trixie release page). For PHP hosting, four things make the difference.
The fix comes as the same PHP, not a new one. Debian moved trixie from 8.4.24 to 8.4.27, a patch release inside the 8.4 branch, and it stays 8.4 for the whole life of Debian 13. Your extensions, your php.ini, your pool files and your applications don't change. The package changelog tells you exactly what changed:
php8.4 (8.4.27-1~deb13u1) trixie-security; urgency=high
* New upstream version 8.4.27
+ (CVE-2026-109618, CVE-2026-109619): Avoid state corruption via
bailout.
It was early. This time Debian's package was built about six hours before PHP's public release and announced 15 minutes before it, because distributions get fixes under embargo. A server that checks the security archive often had the patch before most people had read the news.
One package covers every site. All PHP-FPM pools on a Debian server run from one php8.4-fpm service. Whether you host 2 sites or 200, it is one upgrade and one restart. The package restarts the service by itself.
The automatic updates are built in and free. unattended-upgrades is in Debian's main archive, and its default settings already allow the security archive. You don't need a subscription, an agent or a control panel license. You need two lines of configuration.
The honest downside: Debian 13 gives you PHP 8.4 only. If an application needs PHP 8.5 today, you will add the sury.org repository and you take over the update speed yourself. For most WordPress, Laravel, Symfony, Drupal and Magento sites, 8.4 is supported and the right choice. For the rest of a fresh server (SSH keys, firewall, users), follow our Debian 13 hardening guide first.
Turn on automatic security updates on Debian 13
Every block below runs as root (log in as root, or type sudo -i first). The commands were tested on a fresh Debian 13 server on 22 October 2026.
Step 1: Install the three tools
# as root
apt update
apt install -y unattended-upgrades apt-listchanges needrestart
unattended-upgrades installs the updates, apt-listchanges records what changed, and needrestart finds services that still run old code after an update. The last line of output is about setting up these packages, with no error.
Step 2: Switch the daily run on
# as root
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
EOF
apt-config dump | grep -E "Periodic::(Update-Package-Lists|Unattended-Upgrade) "
Expected output, two lines:
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Unattended-Upgrade "1";
Step 3: Do a dry run
# as root, changes nothing
unattended-upgrade --dry-run -d 2>&1 | grep -E "Allowed origins|Packages that will be upgraded|No packages found"
The first line lists the archives it may install from, and codename=trixie-security must be one of them. On our vulnerable test server the second line was Packages that will be upgraded: nginx nginx-common php8.4-cli php8.4-common php8.4-fpm php8.4-opcache php8.4-readline. On a server that is already up to date, you see No packages found that can be upgraded unattended.
Step 4: Let needrestart restart services without asking
# as root
mkdir -p /etc/needrestart/conf.d
cat > /etc/needrestart/conf.d/50-autorestart.conf <<'EOF'
# Restart services that still use old libraries after an upgrade, without asking.
$nrconf{restart} = 'a';
EOF
cat /etc/needrestart/conf.d/50-autorestart.conf
Keep the single quotes around EOF and around a exactly as shown. Without them the shell eats the $nrconf part, and needrestart later prints a Perl error. We made that mistake in the lab before it worked.
Step 5 (optional): Reboot by itself at 4 in the morning when a kernel update needs it
PHP updates never need a reboot, but kernel and C library updates do. If a short night-time reboot is fine for your sites:
# as root
cat > /etc/apt/apt.conf.d/52unattended-upgrades-local <<'EOF'
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "04:00";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
EOF
apt-config dump | grep -E "Automatic-Reboot|Remove-Unused"
The output repeats the three settings. The server reboots only if /var/run/reboot-required exists after an update. Use a separate file like this one instead of editing 50unattended-upgrades, so a package update never asks you to merge your changes.
If your PHP comes from sury.org, add its origin too. Run apt-cache policy php8.4-fpm, find the o= value on the line for that repository (apt-cache policy without a package name lists every repository with it), and add a block like this to the same file, with that value:
Unattended-Upgrade::Origins-Pattern {
"origin=PUT-THE-o-VALUE-HERE";
};
Run the dry run from step 3 again. The new origin must appear under Allowed origins.
Emergency mode: check for security fixes every hour
The settings above run once a day. Debian's timers fetch package lists around 06:00 and 18:00 with up to 12 hours of random delay, and install around 06:00 with up to an hour of delay. On our fresh server the next install was scheduled 18 hours away. That is fine for an ordinary month. It is too slow for a week like this one, when attacks started before the fix was out.
Emergency mode makes the server look for fixes every hour and install them 20 minutes later:
# as root
mkdir -p /etc/systemd/system/apt-daily.timer.d /etc/systemd/system/apt-daily-upgrade.timer.d
cat > /etc/systemd/system/apt-daily.timer.d/override.conf <<'EOF'
[Timer]
OnCalendar=
OnCalendar=*-*-* *:00
RandomizedDelaySec=10m
EOF
cat > /etc/systemd/system/apt-daily-upgrade.timer.d/override.conf <<'EOF'
[Timer]
OnCalendar=
OnCalendar=*-*-* *:20
RandomizedDelaySec=10m
EOF
cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
APT::Periodic::Update-Package-Lists "always";
APT::Periodic::Unattended-Upgrade "always";
EOF
systemctl daemon-reload
systemctl restart apt-daily.timer apt-daily-upgrade.timer
systemctl list-timers apt-daily.timer apt-daily-upgrade.timer --no-pager
Both timers should now show a next run within the hour. Ours, right after the change:
NEXT LEFT UNIT ACTIVATES
Thu 2026-10-22 11:23:38 UTC 1min 3s apt-daily-upgrade.timer apt-daily-upgrade.service
Thu 2026-10-22 12:01:43 UTC 39min apt-daily.timer apt-daily.service
The word always matters. With "1", apt keeps a timestamp and still runs only once a day, even when the timer fires every hour. The empty OnCalendar= line clears Debian's default schedule before the new one is set. The random delay of up to 10 minutes keeps all servers from hitting the mirror in the same second. Each hourly run fetches only the small index files, unless there is something to install.
To go back to normal later, delete the two override.conf files, put "1" back in 20auto-upgrades, then run systemctl daemon-reload. Many admins simply keep the hourly mode.
Patch right now, without waiting for the timer
# as root
apt update
unattended-upgrade -v
grep -E "Packages that will be upgraded|All upgrades installed" /var/log/unattended-upgrades/unattended-upgrades.log | tail -2
On our test server the log showed the whole run, seven packages including PHP, taking 12 seconds:
2026-10-22 11:21:59,730 INFO Packages that will be upgraded: nginx nginx-common php8.4-cli php8.4-common php8.4-fpm php8.4-opcache php8.4-readline
2026-10-22 11:22:11,824 INFO All upgrades installed
200 websites on a VDS Small, patched all at once: our test
The question we hear from agencies is whether one mid-size server can carry a few hundred small client sites and still stay patched without someone logging in. We built it on our Amsterdam lab server in a Debian 13 container limited to 4 vCPUs and 8 GB of RAM, the same size as our VDS Small plan, and started it on PHP 8.4.24 with the security archive switched off, so it was vulnerable on purpose.
Each of the 200 sites got its own Linux user, its own home folder, its own PHP-FPM pool with open_basedir locked to that folder, and its own nginx server block. One hacked site can't read the files of the other 199: a test script in site001 that tried to read site002's index.php got bool(false). The pools use pm = ondemand, so a site with no visitors has no PHP process at all, and a busy one starts up to 4 workers.
| Test | Result |
|---|---|
| Create 200 users, pools and nginx sites | 8.4 seconds, config tests OK |
| RAM used, all 200 sites idle | about 40 MB |
| 4,000 requests, 20 to every site, 50 at a time | 4,000 answered with HTTP 200 |
| Peak during that test | 673 PHP-FPM processes, 706 MB RAM used |
| 15 seconds after the test | 3 processes, 48 MB RAM used |
| One site, 2,000 requests, 100 at a time (ab) | 11,556 requests per second, 0 failed |
| Automatic security update, 7 packages | 12 seconds from start to "All upgrades installed" |
| Sites on the fixed PHP after it | 200 of 200 report 8.4.27 |
| PHP-FPM restart with 200 pools | 0.19 seconds; 400 requests sent during it, 400 answered with HTTP 200 |
Be fair with these numbers: our test page prints one line. A WordPress page needs much more. In our WordPress on a VPS guide, each PHP worker in a WooCommerce test used about 100 MB. On 8 GB, after the system and MariaDB, that leaves room for roughly 50 to 60 WordPress workers busy at the same moment. Page caching keeps most visits from needing a worker at all. So 200 brochure sites, landing pages and small WordPress sites with a cache fit on a VDS Small. 200 busy shops don't, and they need a bigger plan or a second server. Disk is not the limit: 240 GB of NVMe leaves about a gigabyte per site with space to spare.
One setting we tried and dropped: PHP-FPM's server-wide process.max, which caps the total number of PHP processes. Set to 120 with on-demand pools, it made 246 of 600 requests wait until nginx gave up with a 504 error. Keep the per-site pm.max_children instead, and size it per client.
A VDS has dedicated vCPUs, which is why we name it here and not a VPS. When an update restarts PHP and 200 sites warm up their caches at the same time, those 4 cores are yours alone.
Build the same setup: one site
Install the web stack once, as root:
# as root
apt install -y nginx php8.4-fpm php8.4-mysql php8.4-xml php8.4-mbstring php8.4-curl php8.4-zip php8.4-gd php8.4-intl
Then each site is a user, a folder, a pool and an nginx block. Replace site001 and example.com with your own names:
# as root
u=site001
d=example.com
useradd --system --create-home --home-dir /srv/$u --shell /usr/sbin/nologin $u
mkdir -p /srv/$u/public
echo '<?php echo "PHP ", PHP_VERSION, "\n";' > /srv/$u/public/index.php
chown -R $u:$u /srv/$u
chmod 750 /srv/$u
usermod -aG $u www-data
cat > /etc/php/8.4/fpm/pool.d/$u.conf <<EOF
[$u]
user = $u
group = $u
listen = /run/php/$u.sock
listen.owner = www-data
listen.group = www-data
pm = ondemand
pm.max_children = 4
pm.process_idle_timeout = 10s
pm.max_requests = 500
php_admin_value[open_basedir] = /srv/$u/public:/tmp
EOF
cat > /etc/nginx/sites-available/$d <<EOF
server {
listen 80;
server_name $d www.$d;
root /srv/$u/public;
index index.php index.html;
location ~ \.php\$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/$u.sock;
}
}
EOF
ln -s /etc/nginx/sites-available/$d /etc/nginx/sites-enabled/$d
php-fpm8.4 -t && nginx -t && systemctl reload php8.4-fpm nginx
curl -s -H "Host: $d" http://127.0.0.1/
Both tests must end with test is successful, and curl prints PHP 8.4.27. The \$ in the nginx block is on purpose: it keeps a real $ in the file. Add HTTPS with certbot or a reverse proxy afterwards; our reverse proxy and free SSL guide covers both.
Check all sites after an update
Put a small index.php like the one above (or a hidden /phpver.php) in every site, then ask all of them at once:
# as root; prints how many sites answer with each PHP version
for f in /etc/nginx/sites-enabled/*; do
d=$(basename "$f")
[ "$d" = default ] && continue
curl -s -H "Host: $d" http://127.0.0.1/ | awk '{print $NF}'
done | sort | uniq -c
Our lab, right after the automatic update, answered with one line: 200 8.4.27. Delete the test file from sites that run a real application, or protect it, so you don't publish your PHP version to the world.
Frequently asked questions
What is PHP2Shell?
PHP2Shell is the name for two critical bugs in the PHP engine, CVE-2026-109618 and CVE-2026-109619, both rated CVSS v4.0 9.2. A crafted upload request can make PHP fail halfway through a request and reuse freed memory. That lets an attacker run code on the server without logging in and can leak data from other requests. They were fixed on 21 October 2026 in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35.
Which Debian version fixes PHP2Shell?
On Debian 13 (trixie), install php8.4 version 8.4.27-1~deb13u1 or later from trixie-security, announced in DSA-6574-1 on 21 October 2026. On Debian 12 (bookworm), the fix is php8.2 8.2.35-1~deb12u1 from DLA-4848-1. Run apt update && apt upgrade and check with apt-cache policy php8.4-fpm.
Do I need to restart anything after the PHP update?
The Debian package restarts php8.4-fpm by itself, and with needrestart in automatic mode, other services that loaded the old library are restarted too. In our test the PHP-FPM restart took 0.19 seconds with 200 pools, and no request failed. Only kernel and core library updates need a reboot.
Is a WAF rule enough instead of updating?
No. Cloudflare added managed WAF rules for PHP2Shell (WAF changelog), and they block known attack patterns. But the bug is in how PHP reads any upload request, and PHP calls every workaround partial. Update PHP. Keep the WAF rule as an extra layer.
How many websites can a 4 vCPU, 8 GB server host?
In our test, 200 small PHP sites with one PHP-FPM pool each used about 40 MB of RAM while idle and 706 MB under a burst of 4,000 requests, because on-demand pools only run PHP while a site has visitors. Real limits depend on how many sites are busy at the same moment: with WordPress workers at about 100 MB each, plan for 50 to 60 at once, and cache your pages.
Will automatic updates break my PHP sites?
The default settings install only security updates from Debian's stable archives, which stay on the same PHP version (8.4 for the whole life of Debian 13). That makes breakage rare. Run the dry run before you turn it on, keep backups, and read /var/log/unattended-upgrades/unattended-upgrades.log after a big security week.
Turn it on today, then forget about it
PHP2Shell was under attack before the fix was public, and it won't be the last bug like that. A Debian 13 server with unattended-upgrades in hourly mode would have been patched by itself shortly after 13:45 UTC on 21 October, every site at once, while its owner was asleep. If you want a server built for this, our VDS plans come with dedicated vCPUs, NVMe, a 10 Gb/s port and free weekly backups. They are ready with Debian 13 in under 60 seconds, in Amsterdam, Dublin or Prishtina. Not sure how many sites your plan can carry? Ask us on Telegram.