Skip to content
Servers

How to Fix and Mitigate the PHP2Shell Vulnerability on Your Server

In this article13 sections

On 21 October 2026, the PHP project published fixes for two critical flaws in PHP, CVE-2026-109618 and CVE-2026-109619, which the community is calling php2shell. Both are rated 9.2 on CVSS v4.0 and can allow remote code execution by an unauthenticated attacker. The advisories say every PHP installation that serves web requests is affected, including PHP-FPM, Apache with mod_php, and php-cgi.

This guide covers what the flaws are at a high level, how to find out whether your server is exposed, how to patch, and every mitigation that works in front of or alongside a patch. The only complete fix is to update PHP. The other measures reduce exposure while you patch, or add layers if a patch cannot be applied yet. The guide also explains what to check if you suspect a server was attacked, because the exposure window started before the public disclosure.

Act now. Amazon Threat Intelligence recorded an attempt against its honeypot network on 17 October 2026, four days before disclosure, and more attempts on 21 October. Cloudflare reported early attacks. Treat any internet-facing PHP server as exposed until you have confirmed the patched version, and check it for signs of compromise from 17 October onward.

What the flaws are

The advisories describe the two flaws at a high level. The exploit details are not published here, and you do not need them to protect a server.

  • CVE-2026-109618 is a memory-handling flaw. When memory runs out during the start of a request, PHP can continue with partly set-up internal data, as if it were valid. That is the kind of state an attacker can use.
  • CVE-2026-109619 is a stale-state flaw. After a fatal error, some request-owned data can survive into the next request handled by the same worker. Two cases are confirmed in the advisories: fatal-error backtraces on PHP 8.5, and upload processing, including session upload progress, which is enabled by default.

Both flaws can be triggered by a crafted HTTP POST request, with no login. That is why the exposure is so broad: a PHP site is reachable by anyone on the internet.

Which versions are affected and what fixes them

PHP branch Vulnerable below Fixed in
8.5 8.5.12 8.5.12
8.4 8.4.27 8.4.27
8.3 8.3.36 8.3.36
8.2 8.2.35 8.2.35
8.1 and older No official release Source backports exist for 7.4 to 8.1 (for people who build from source). Versions 7.3 and older get no fix from the PHP project.

The PHP changelog lists 8.4.27 and 8.5.12 dated 22 October 2026. Confirm the CVE IDs against the PHP release notes on php.net before you finish, because the changelog page is long and this guide could not read every entry.

Linux distributions can ship a fix under the old version number, with a backported patch. Ubuntu 24.04 ships PHP 8.3 with its own package revision. A version number alone does not prove a patch is present, so check the package changelog as shown in Part 2.

Part 1: Find every PHP installation

Before you patch, make a list of every PHP runtime on the machine. A single server often has several: the system PHP for the command line, a PHP-FPM version per site, an Apache module, and PHP bundled inside an application or container. Each one needs to be checked.

php -v
ls /etc/php/
dpkg -l | grep -E "php[0-9.]+-(fpm|cli|common)" | awk '{print $2, $3}'
systemctl list-units --type=service | grep -i php

The dpkg line lists installed PHP packages with their versions. The systemctl line shows which PHP-FPM services are running. Note every version you see.

Next, find the PHP version that the running web workers use, not only the one installed. A server can have 8.4 installed while a site still runs 8.2:

ps -eo pid,user,args | grep -E "php-fpm|php-cgi|apache2" | grep -v grep

Check the Apache module and the PHP-FPM version the web server calls:

apache2ctl -M 2>/dev/null | grep -i php
grep -rh "fastcgi_pass" /etc/nginx/ 2>/dev/null | sort -u

Each fastcgi_pass line names a PHP-FPM socket, such as php8.4-fpm.sock, which tells you the version that site uses.

Check PHP inside applications and containers

Some applications bundle their own PHP binary, and containers carry a base image with PHP inside. The advisories warn that these are easy to miss. Look for PHP binaries and images:

find / -xdev -type f -name "php*" -perm -u+x 2>/dev/null | grep -v "/usr/share" | head -50
docker images | grep -i php

For each container image, check the PHP version inside the running container, not the one on the host:

docker exec <container-name> php -v

Part 2: Patch the installed PHP

Ubuntu and Debian

Refresh the package lists, then see which PHP packages have an update waiting:

sudo apt update
apt list --upgradable 2>/dev/null | grep -i php

If you use the Ondřej Surý PHP repository, which many Ubuntu servers use for PHP 8.4 and newer, its packages carry the upstream version numbers and should show 8.4.27 or 8.5.12 after the update. Confirm the repository is present:

grep -rh "ondrej" /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null

Upgrade the PHP packages you use, then check the version the system now reports:

sudo apt install --only-upgrade php8.4-fpm php8.4-cli php8.4-common
php8.4 -v

Replace 8.4 with each branch you run. The expected output starts with PHP 8.4.27 or later. If the output still shows an older version, the upgrade did not apply, so check the repository and run the commands again.

On Ubuntu 24.04, the default php8.3 package is patched through Ubuntu’s security notices, under its own revision number. Check the package changelog for the CVE:

apt changelog php8.3 2>/dev/null | grep -i "109618\|109619" | head

If that prints nothing, it does not prove the package is unpatched. Ubuntu’s security tracker is the reliable source. Look up the CVE on the Ubuntu CVE page for the package and the release, and install the newest update Ubuntu publishes for php8.3.

Restart every worker after the upgrade

Patching the package does not change a worker that is already running. Restart each PHP-FPM service and the web server that loads PHP:

sudo systemctl restart php8.4-fpm
sudo systemctl restart apache2   # only if Apache runs mod_php
sudo systemctl restart nginx     # only if you changed nginx configuration

Confirm the workers now run the new version by checking the process start time and the binary path:

ps -eo pid,lstart,args | grep "php-fpm" | grep -v grep | head -3

Expected: each worker’s start time is after the upgrade. A worker that started before it is still running the old code.

Other platforms

  • cPanel and WHM: run EasyApache or the MultiPHP manager update, and check the PHP version for each domain. Confirm with your host that the update reached every PHP version the account uses.
  • Plesk: update PHP through the Tools and Settings updates, and check each subscription’s PHP handler.
  • Docker images: rebuild every image on a current PHP base image, then redeploy the containers. Restarting a container from the old image keeps the old PHP.
  • Source builds: build from 8.4.27 or later, or apply the source backport for your branch if you must stay on an older branch.
  • Managed hosting: ask your provider to confirm the PHP version on your account in writing. Do not assume it has been updated.

Part 3: Apply the configuration mitigations

The advisories name three settings that reduce exposure to the specific paths the flaws use. They are not fixes, and they do not replace patching. Apply them to every PHP installation that you cannot patch immediately, and keep them after you patch if you want the extra margin.

Find the configuration file for each runtime

php --ini
ls /etc/php/8.4/fpm/php.ini /etc/php/8.4/cli/php.ini /etc/php/8.3/apache2/php.ini 2>/dev/null

Edit the php.ini that matches the runtime that serves requests. For PHP-FPM, that is the fpm/php.ini file, not the CLI one. Apache mod_php uses apache2/php.ini.

Disable session upload progress

This addresses CVE-2026-109619. Upload progress tracking is on by default and runs even when the application never starts a session, which is why the advisory calls it out:

session.upload_progress.enabled = 0

Your application may use upload progress bars. Test an upload after the change. If an upload progress feature stops working, the cost of keeping this setting off is one feature, which is a small price.

Turn off fatal-error backtraces on PHP 8.5

This also addresses CVE-2026-109619, on PHP 8.5 only:

fatal_error_backtraces = 0

Backtraces help developers debug. Keep them on a development server, and turn them off on any server that faces the internet.

The memory limit, with a warning

PHP’s advisory suggests memory_limit = -1 to address CVE-2026-109618, set so that applications cannot override it. This removes PHP’s own cap on memory per request. That changes the failure mode: a runaway request can use all the memory the operating system gives it, which can take down other sites on the same machine.

Use this setting only with the other limits in place:

  • A memory limit on the container, cgroup or systemd service, so a request cannot take the whole machine down.
  • A request body size limit at the web server, which the next part covers.
  • Monitoring for memory use, so you notice a spike.

On a single-purpose server with those limits in place, it is a reasonable interim measure. On a shared server with many accounts, prefer the other mitigations and the patch, and ask your host before changing the memory limit on your account.

Apply and verify

Restart the runtime after every change, because PHP reads these settings at startup:

sudo systemctl restart php8.4-fpm
php -i | grep -E "upload_progress.enabled|memory_limit|fatal_error_backtraces"

Check the PHP-FPM workers too, since the CLI setting does not change them. For PHP-FPM, read the effective pool configuration:

sudo php-fpm8.4 -tt 2>&1 | grep -E "upload_progress|memory_limit"

Do not use ulimit, RLIMIT_AS or RLIMIT_DATA as a substitute for the memory limit. The advisory warns that these can trigger the same allocation failures the flaw depends on, which makes the problem worse rather than better.

Part 4: Limit what can reach PHP

Most attacks have to reach PHP through the web server. Limiting what the web server passes on removes a large share of the risk, without touching the application.

Limit the request body size

The exploit uses a crafted form body. A form on a typical site does not need a large body, so set a limit for normal pages and raise it only for the places that really take large uploads:

server {
    server_name example.com;
    client_max_body_size 2m;

    location /upload/ {
        client_max_body_size 64m;
    }
}

Adjust the numbers to your application. Test a normal form submission and an upload after the change, then reload Nginx with sudo nginx -t && sudo systemctl reload nginx.

Apache has the equivalent LimitRequestBody directive. Set it in the virtual host, with a larger value only for the upload paths.

Block malformed requests with a web application firewall

A WAF inspects requests before they reach PHP. Vendors have published rules for these flaws. Cloudflare has deployed managed rules for its customers, which are available on every plan, including the free one. Cloudflare describes these rules as a stopgap: they cover known patterns only, and they protect only traffic that passes through Cloudflare.

For a self-managed WAF such as ModSecurity with the OWASP Core Rule Set, keep the rule set current and check its logs for blocked requests during the next few days. A WAF blocks requests that match a pattern, so an unusual but legitimate form can be blocked as well. Review the blocked requests and add exceptions only for paths you have verified.

Check that your proxy or WAF does not truncate or drop the parts of the request it needs to inspect. Long field names and unusual control bytes are the signs the advisories describe, so a rule that silently drops them is not doing its job.

Put the origin behind a proxy

If your site is behind Cloudflare, the attacker can still reach the origin server directly if they know its IP address. Restrict the origin to the proxy’s addresses. Cloudflare publishes its IP ranges at cloudflare.com/ips. Allow ports 80 and 443 only from those ranges, and keep SSH on its own rule:

sudo ufw allow from 173.245.48.0/20 to any port 443 proto tcp
sudo ufw allow from 103.21.244.0/22 to any port 443 proto tcp
sudo ufw status numbered

The two lines above are examples. Copy the full, current list of ranges from Cloudflare’s published page, add every range for ports 80 and 443, then remove the old rules that allow everyone. Test from a second network before you close your current session. A mistake here locks out your own visitors, not only attackers.

Isolate the server

The CERT-EU guidance recommends isolating internet-facing applications from other networks until the fix is applied and verified. In practice that means: move admin tools, databases and backups off the public interface, put internal services on a private network, and do not run a PHP application next to a server that holds credentials for other systems. If you cannot isolate the machine, prioritise patching it.

Part 5: Limit the damage if a worker is compromised

A patch and a WAF reduce the chance of an attack. The next layer limits what an attacker can do if one succeeds.

  • Run each site as its own user. PHP-FPM pools let each site run under a separate account, so a compromised site cannot read another site’s files. Check each pool’s user and group in /etc/php/8.4/fpm/pool.d/.
  • Keep the web root read-only where you can. Only the upload and cache folders should be writable by PHP. Code folders should belong to a deploy user and be read-only to the web user.
  • Disable functions you do not use. The disable_functions setting can block commands such as exec, shell_exec and proc_open. Test thoroughly, because many applications use them, and treat this as defence in depth, not a barrier that stops a determined attacker.
  • Use systemd resource limits. A MemoryMax= line in a unit override stops a runaway PHP-FPM service from exhausting the machine. Set it on the PHP-FPM service after you have measured normal memory use.
  • Restrict outbound connections. A compromised web worker often downloads a second-stage tool. If the server does not need to reach arbitrary addresses, a firewall that allows only the destinations you need removes that step.

Part 6: Check whether a server was already attacked

Patching closes the door. It does not show whether someone walked through it. The advisories say investigations should start from 17 October 2026, not from the disclosure date. Work through the checks below for every PHP server that was exposed in that window.

Look for PHP workers that started a shell

The clearest sign in the advisory is a web worker that launches a command shell. This command lists the children of every PHP-FPM and Apache process:

for p in $(pgrep -f "php-fpm|apache2"); do pgrep -P "$p" -a; done

Expected on a healthy server: no output, or only short-lived helper processes that you recognise. Investigate any sh, bash, dash, curl or wget child of a web worker.

Look for crash and memory errors in the logs

The advisories describe memory exhaustion during request setup and worker segfaults. Search the logs from 17 October onward:

sudo grep -h "Allowed memory size" /var/log/php*-fpm.log /var/log/apache2/error.log* 2>/dev/null | tail -20
sudo grep -h "Segmentation fault" /var/log/apache2/error.log* 2>/dev/null | tail -20
sudo journalctl -u php8.4-fpm --since "2026-10-17" --no-pager | grep -iE "signal|segfault|fatal" | tail -20

A few isolated memory errors can come from normal traffic. A cluster of them at the same time as an unusual request pattern in your access log is worth investigating.

Look at the requests

Search the web server access log for POST requests with very long form data, or with unusual characters, from 17 October onward. Count requests by path and by client address:

sudo awk '$6 == "\"POST" {print $1, $7}' /var/log/nginx/access.log* 2>/dev/null | sort | uniq -c | sort -rn | head -30

This shows the POST requests by client address and path, which helps you spot a source sending many requests to one endpoint. A high count from one address to a path that normally gets few POSTs is a lead to follow. The exact log format varies, so adjust the field numbers to match your access.log lines.

Look for new or changed files

Web shells are PHP files placed in the web root. Look for PHP files modified since 17 October in folders that should not change often:

sudo find /var/www -name "*.php" -newermt "2026-10-17" -not -path "*/cache/*" 2>/dev/null | head -50

Also look for PHP files in upload folders, where only images and documents belong:

sudo find /var/www -path "*/upload*" -name "*.php" 2>/dev/null

A common web-shell signature is a call to eval on decoded input. This search is basic and will miss obfuscated files, so use it as a first pass, not as proof:

sudo grep -rlE "eval\(base64_decode|eval\(gzinflate|eval\(str_rot13" /var/www 2>/dev/null | head -50

Check for new accounts, keys and scheduled jobs

sudo awk -F: '$3 >= 1000 {print $1, $3, $7}' /etc/passwd
sudo ls -la /home/*/.ssh/authorized_keys /root/.ssh/authorized_keys 2>/dev/null
sudo crontab -l -u www-data 2>/dev/null
sudo ls -la /etc/cron.d/ /etc/cron.daily/ 2>/dev/null

Compare the output with your records. An account, key or cron job you did not create is a serious sign, and it needs the response in Part 7.

Check for outbound connections from PHP

sudo ss -tnp | grep -E "php|apache|nginx" | grep -v ":443\|:80" | head -30

Connections from a PHP process to unusual ports or unknown addresses need investigation. Normal PHP traffic goes to your database, your cache and the APIs your application uses, which you can list.

Part 7: If you find signs of compromise

Do not try to clean the server in place. An attacker with code execution can hide persistence in many places, so the reliable response is to contain, preserve and rebuild.

  1. Contain. Take the site offline, or block its traffic at the firewall, while you work. Keep SSH access for yourself only.
  2. Preserve evidence. Copy the logs, the web root and the process list to a separate location before you change anything. Note the time you found the problem.
  3. Rotate every secret. Change database passwords, the application key, API keys in .env files, SSH keys, and the admin passwords of every application on the server. Assume each one was read.
  4. Rebuild, do not repair. Deploy the application and the server from known-good sources onto a fresh machine or container, restore data from a backup taken before 17 October, and check the restored data for injected code.
  5. Report where required. If personal data may have been accessed, check your legal obligations for notifying the regulator and affected people. The time limits can be short.

Restoring a clean backup is only safe if the backup predates 17 October 2026, or if you have verified that the backup is clean. A backup taken after the exposure window can already contain the attacker’s files.

Part 8: Choose the right option for your situation

Your situation What to do first Then
Own server, PHP 8.2 to 8.5, can update now Patch and restart workers (Part 2) Apply the configuration settings, check logs (Part 6)
Own server, PHP 8.1 or older Isolate or take the sites offline, as CISA advises Migrate to a supported branch, then patch
Cannot patch today Config settings (Part 3), request limits and WAF (Part 4) Patch as soon as you can, and keep the limits
Behind Cloudflare, origin reachable Restrict the origin to Cloudflare ranges (Part 4) Enable managed WAF rules, then patch
Shared or managed hosting Ask your host, in writing, for the patched PHP version on your account Check your sites with Part 6 if the host confirms a delay
Docker containers Rebuild images on a patched base Redeploy and check the PHP version inside each container
Suspect a compromise Part 7: contain, preserve, rotate, rebuild Confirm with the Part 6 checks on the new system

A checklist to finish with

  1. Every PHP runtime on the server is listed, including containers and bundled copies.
  2. Every runtime serving requests is at 8.2.35, 8.3.36, 8.4.27, 8.5.12 or later, or has a verified backport.
  3. Every PHP-FPM and web server process was restarted after the update.
  4. The configuration settings are in place, and the workers were restarted after the change.
  5. Request body limits are set, and upload paths are the only exceptions.
  6. A WAF or managed rule set is active, and its blocked requests were reviewed.
  7. The origin accepts traffic only from the proxy, where a proxy is used.
  8. The Part 6 checks were run from 17 October 2026, and the results were recorded.
  9. Logs are kept off the server, so an attacker cannot erase them.

Patching closes this flaw, but it is not the only layer. Allow only the ports you use with how to set up a UFW firewall on Ubuntu, and check the rest of the host against how to audit a new Ubuntu VPS for security. Those steps limit what an attacker can reach while the next flaw is still unpatched.

Frequently asked questions

Is PHP-FPM affected, or only mod_php?

Both. The advisories say PHP-FPM is affected, and mod_php is the higher-risk case because the flaw can be reached before any application code runs. Patch the PHP-FPM service, not only the Apache module.

Does a patched version mean I am safe?

For these two flaws, yes, once every running worker uses the patched code. It does not prove that the server was not attacked before the patch, which is why Part 6 matters.

Is the command-line PHP affected?

The CLI is not the main target of the public advisories, which focus on web requests. Patch it anyway, because the same package provides the web runtime, and scripts run from cron can be affected by the same bugs.

Does the Cloudflare rule protect my server?

Only the traffic that goes through Cloudflare, and only for the patterns its rules cover. Patch the server, and restrict the origin, so the protection cannot be bypassed.

I run an older PHP that is out of support. What now?

Plan a migration to a supported branch. Until then, follow the isolation advice in Part 4 and the CISA guidance, and take sites offline if they are not essential. Source backports exist for older branches, but they are for people who build PHP themselves, and they still need a full test.

Sources

The version numbers and CVE IDs above were cross-checked across these sources. The PHP release notes on php.net remain the authoritative record. Check them, and your distribution’s security notices, before you finish.

Patch every PHP runtime to 8.2.35, 8.3.36, 8.4.27 or 8.5.12 or later, restart every worker, and then check the server for signs of compromise from 17 October 2026. Use the configuration settings, request limits and proxy rules to reduce exposure while you patch, and treat them as temporary layers, not as the fix.

Share this guide