Exploited in the wild. Updated
PHP2Shell: check, fix and mitigate
- CVE-2026-109618
- CVE-2026-109619
- CVSS 4.0 9.2
PHP2Shell is a pair of unauthenticated remote code execution flaws in PHP itself. One crafted HTTP request can hand an attacker a shell on any server running PHP 5.4 to 8.5, whatever the application on top: WordPress, Drupal, Magento or your own code.
Fixed upstream in 8.5.12 8.4.27 8.3.36 8.2.35. PHP 8.1 and older got no release: they need a vendor backport, such as the free GetPageSpeed builds for CentOS 7.
Is my server vulnerable?
This checker knows about distro backports, so it can tell you when a fixed package still shows an old PHP version.
How to fix PHP2Shell on your platform
Updating PHP is the only complete fix. Install your vendor's security update, then restart every PHP process: running workers keep the vulnerable code in memory until they restart.
Ubuntu backports the fix and keeps the PHP version it shipped, so php -v on a patched Ubuntu 24.04 still says 8.3.6. Compare the package version instead.
| Release | Package | First fixed version | Advisory |
|---|---|---|---|
| Debian 13 trixie | php8.4 | 8.4.27-1~deb13u1 | DSA-6574-1 |
| Debian 12 bookworm | php8.2 | 8.2.35-1~deb12u1 | DLA-4848-1 |
| Ubuntu 26.10 | php8.5 | 8.5.9-0ubuntu2.1 | USN-8948-1 |
| Ubuntu 26.04 LTS | php8.5 | 8.5.4-0ubuntu1.5 | |
| Ubuntu 24.04 LTS | php8.3 | 8.3.6-0ubuntu0.24.04.13 | |
| Ubuntu 22.04 LTS | php8.1 | 8.1.2-1ubuntu2.28 | |
| Ubuntu 20.04 (Pro) | php7.4 | 7.4.3-4ubuntu2.29+esm5 | Ubuntu Pro |
sudo apt update
sudo apt install --only-upgrade $(dpkg -l 'php*' | awk '/^ii/{print $2}')
sudo systemctl restart 'php*-fpm.service' apache2 # restart what you run
apt changelog php8.3-fpm | grep -E 'CVE-2026-10961[89]' # verify
Debian 11 bullseye (PHP 7.4) had no published fix when this page was updated.
Red Hat, AlmaLinux and Rocky Linux shipped the same fixes. Alma and Rocky add their own build tags to module packages, so compare the version and release before the .module part.
| Release | Stream | First fixed | Red Hat advisory |
|---|---|---|---|
| EL 10 | php (8.3) | 8.3.36-1.el10_2 | RHSA-2026:87070 |
| EL 10 | php8.4 | 8.4.27-1.el10_2 | RHSA-2026:87071 |
| EL 9 | php:8.3 module | 8.3.36-1 | RHSA-2026:87079 |
| EL 9 | php:8.2 module | 8.2.35-1 | RHSA-2026:87072 |
| EL 9 | base php (8.0) | 8.0.30-8.el9_8.1 | RHSA-2026:87151 |
| EL 9 | php:8.4 module | No fix yet | Listed as affected |
| EL 8 | php:8.2 module | 8.2.35-1 | RHSA-2026:87073 |
| EL 8 | php:7.4 module | 7.4.33-8 | RHSA-2026:87080 |
sudo dnf updateinfo list --cve CVE-2026-109618 # shows the fix for your stream sudo dnf upgrade 'php*' sudo systemctl restart php-fpm httpd
On the RHEL 9 php:8.4 stream, switch to the fixed php:8.3 stream with dnf module switch-to php:8.3 until Red Hat ships a fix.
No distribution or upstream fix exists for CentOS 7. GetPageSpeed publishes free, patched builds of Remi's PHP 5.6, 7.0, 7.3, 7.4 and 8.0 packages for CentOS 7, plus a PHP 8.4 stack. Details are in the CentOS 7 section below.
yum -y install https://extras.getpagespeed.com/release-latest.rpm yum --enablerepo=getpagespeed-extras-php74 upgrade 'php-*' # or: yum upgrade 'php74-php*' systemctl restart php-fpm
Remi (EL 8 and newer) shipped 8.2.35, 8.3.36, 8.4.27 and 8.5.12 for both module streams and software collections, plus backports in php74-php 7.4.33-31 and php80-php 8.0.30-21. For CentOS 7 Remi has nothing: see the CentOS 7 tab.
sudo dnf upgrade 'php*' sudo systemctl restart php-fpm # SCL: systemctl restart php83-php-fpm
Ondřej Surý's packages (packages.sury.org and the Ubuntu PPA) shipped 8.2.35, 8.3.36, 8.4.27 and 8.5.12 on release day. When this page was last updated, the 8.1, 8.0, 7.4 and 5.6 builds there had no PHP2Shell entry in their changelogs, so treat them as unpatched until apt changelog says otherwise.
sudo apt update && sudo apt install --only-upgrade php8.3-fpm php8.3-cli apt changelog php8.3-fpm | grep -E 'CVE-2026-10961[89]'
cPanel EasyApache 4 (build 25.91, October 21) updated ea-php82 to ea-php85 to the fixed releases and backported the fix to ea-php74, ea-php80 and ea-php81. See the EasyApache 4 change log.
yum update 'ea-php*' # or: dnf update 'ea-php*' /scripts/restartsrv_apache_php_fpm
Plesk released PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35, and backported the fix to its PHP 8.1.34, 8.0.30 and 7.4.33 builds. Updates install automatically. To force one, see the Plesk change log.
plesk installer --select-release-current --upgrade-installed-components
- Alpine:
php82tophp85are fixed at8.2.35-r0,8.3.36-r0,8.4.27-r0and8.5.12-r0on the supported branches. Runapk update && apk upgrade. - Amazon Linux 2023: php8.2 to php8.5 are fixed (ALAS2023-2026-3301 to 3304). Run
sudo dnf upgrade --releasever=latest 'php*'. - Official Docker images: rebuild from
php:8.5.12,php:8.4.27,php:8.3.36orphp:8.2.35tags (or the moving8.xtags), then redeploy. Images on 8.1 and older will not be fixed. - Homebrew and Windows: upgrade to the fixed upstream release (
brew upgrade php, or a new build from windows.php.net).
PHP2Shell on CentOS 7 and end-of-life PHP
php.net fixed only PHP 8.2 and newer. Remi stopped building for CentOS 7 in June 2024, so Remi's PHP 5.6, 7.x and 8.0 packages for CentOS 7 will never get this fix. GetPageSpeed rebuilt them with PHP2Shell backported. Package names stay the same, so yum upgrade replaces them in place, and PECL extensions and ionCube keep working.
| PHP | Patched build | Where it comes from |
|---|---|---|
| 5.6 | 5.6.40-43.el7 | PHP 5.6 adaptation of the upstream fix |
| 7.0 | 7.0.33-45.el7 | port of the official 7.4 fix |
| 7.3 | 7.3.33-25.el7 | official 7.4 fix, hunk for hunk |
| 7.4 | 7.4.33-33.el7 | official 7.4 fix |
| 8.0 | 8.0.30-24.el7 | official 8.0 backport |
| 8.4 | 8.4.27-2.el7 | upstream PHP 8.4.27 |
Base-name packages (remi-php56 to remi-php80)
yum -y install https://extras.getpagespeed.com/release-latest.rpm yum --enablerepo=getpagespeed-extras-php74 upgrade 'php-*' systemctl restart php-fpm
Replace php74 with php56, php70, php73 or php80. Enable the channel only for this transaction, as shown.
Software collections (php56-php to php80-php)
yum -y install https://extras.getpagespeed.com/release-latest.rpm yum upgrade 'php74-php*' systemctl restart php74-php-fpm
Collection builds come from the main GetPageSpeed repository, so no extra channel is needed. Afterwards php -v prints Built by GetPageSpeed LLC. Running mod_php? Restart httpd instead.
- Free. No subscription is needed for the PHP channels.
- Validated. On a clean CentOS 7 system, both PHP2Shell probes reproduce on the Remi package and fail on the patched build. The phpt suite and the module list match the Remi baseline.
- More than PHP2Shell. The builds also carry 2024 to 2026 security fixes that CentOS 7 never got.
- Reversible.
yum downgradetakes you back to Remi. - One behaviour change. A request that hits
memory_limitnow ends its worker immediately, the same trade-off the PHP team made.
Want to leave EOL PHP behind? The PHP 8.4 stack for CentOS 7 is already at 8.4.27:
yum-config-manager --enable getpagespeed-extras-php84 yum -y install php84-getpagespeed-stack
Read the CentOS 7 PHP2Shell guide
Still running CentOS 7? The GetPageSpeed CentOS 7 LTS repository keeps current NGINX and modules installable too.
Mitigate PHP2Shell at the NGINX edge
NGINX cannot fix a bug inside PHP, but it decides what reaches PHP. We tested each of these controls on Rocky Linux 10 with NGINX 1.30 and PHP-FPM 8.3. Each was checked against the public proof-of-concept request shape and against normal uploads. Use them to buy time and to harden patched servers too.
Good news for NGINX users. One CVE-2026-109618 strategy relies on a multipart body "without a usable declared content length" slipping past post_max_size. With the default fastcgi_request_buffering on, NGINX de-chunks and buffers the body, then passes PHP-FPM the real length. In our tests a chunked 100 KB upload reached PHP with CONTENT_LENGTH=102767, and post_max_size applied normally. Keep buffering on for PHP locations.
1. Cap the request body size
This is the strongest single control. NGINX counts de-chunked bytes as they arrive and rejects an oversized body with 413 before PHP parses anything. In testing, the 5 MB proof-of-concept body was cut off at 1 MB, with this log line: client intended to send too large chunked body.
location ~ \.php$ {
client_max_body_size 1m; # the smallest size your uploads need
fastcgi_request_buffering on; # the default: never stream bodies to PHP
include fastcgi_params;
fastcgi_pass unix:/run/php-fpm/www.sock;
}
2. Reject chunked multipart uploads
Browsers always send Content-Length with form uploads, but the public exploit uses chunked encoding. Refuse that combination for PHP before NGINX reads the body:
# http {} context, for example /etc/nginx/conf.d/00-php2shell.conf
map $http_content_type $p2s_multipart { default ""; "~*^multipart/form-data" "m"; }
map $http_transfer_encoding $p2s_chunked { default ""; "~*chunked" "c"; }
# inside the PHP location {}
set $p2s_block "$p2s_multipart$p2s_chunked";
if ($p2s_block = "mc") { return 411; }
Tested results: a chunked multipart request got 411, while normal multipart and urlencoded posts got 200. This rule covers HTTP/1.1 only. HTTP/2 has no chunked encoding, and we did not test it there, so rely on the body cap for HTTP/2.
3. Turn off session upload progress the right way
A common suggestion is fastcgi_param PHP_VALUE "session.upload_progress.enabled=0". It does not work. In our test PHP still wrote upload_progress_* data to the session file. The setting has to be applied at the admin level:
; PHP-FPM pool, e.g. /etc/php-fpm.d/www.conf or /etc/php/8.3/fpm/pool.d/www.conf php_admin_flag[session.upload_progress.enabled] = off
fastcgi_param PHP_ADMIN_VALUE "session.upload_progress.enabled=0"; also worked in testing. Prefer the pool setting: the advisory warns that per-request INI values from the frontend which raise a diagnostic are themselves a CVE-2026-109619 trigger. Audit what your NGINX sends:
grep -rn 'PHP_VALUE\|PHP_ADMIN_VALUE' /etc/nginx /etc/php-fpm.d /etc/php 2>/dev/null
4. Block the exploit fields with ModSecurity
For a request-body filter, the GetPageSpeed nginx-module-security package (ModSecurity v3) inspects multipart field names, including in chunked bodies. These rules returned 403 for a PHP_SESSION_UPLOAD_PROGRESS field, a 2,000-byte field name and a NUL byte, and 200 for normal uploads:
# /etc/nginx/php2shell.rules Include /etc/nginx/modsecurity.conf SecRule ARGS_NAMES "@rx (?i)^PHP_SESSION_UPLOAD_PROGRESS$" \ "id:1000100,phase:2,t:none,deny,status:403,log,msg:PHP2Shell-upload-progress-field" SecRule ARGS_NAMES "@gt 1024" \ "id:1000101,phase:2,t:none,t:length,deny,status:403,log,msg:PHP2Shell-oversized-arg-name" SecRule ARGS|ARGS_NAMES "@rx \x00" \ "id:1000102,phase:2,t:none,deny,status:403,log,msg:PHP2Shell-nul-byte"
# nginx.conf (top level)
load_module modules/ngx_http_modsecurity_module.so;
# inside the PHP location {}
modsecurity on;
modsecurity_rules_file /etc/nginx/php2shell.rules;
The package's base modsecurity.conf already turns on strict multipart parsing, which rejected malformed and empty-name multipart bodies with 400. Install it with dnf install nginx-module-security from the GetPageSpeed repository. NAXSI (nginx-module-naxsi) can block the field name too, but it has no length check, so ModSecurity is the better fit here.
Edge rules are not a patch. They stop the known request shapes. The advisory documents other paths that need no special upload at all. Update PHP.
Temporary php.ini mitigations, and why they are not enough
If you cannot update today, the PHP security team lists these settings. Apply them administratively (php.ini or the FPM pool, not per request), then restart every worker. Each one closes a single known path. None of them is a fix.
| Setting | What it blocks | What it leaves open |
|---|---|---|
session.upload_progress.enabled = 0 | The session upload progress path of CVE-2026-109619, confirmed against mod_php and FPM | Every other stale-state path, and all of CVE-2026-109618 |
fatal_error_backtraces = 0 (PHP 8.5) | The producer behind the strongest confirmed CVE-2026-109619 exploit | Other error, session and extension state |
memory_limit = -1 | The predictable, controllable out-of-memory trigger for CVE-2026-109618 | Real allocation failures still reach the vulnerable code, and one request can now eat all RAM |
Low post_max_size, max_input_nesting_level and max_input_vars | Cheaper paths to memory exhaustion | Application-specific memory exhaustion |
pm.max_requests = 1 (FPM) | State carried from one request to the next on the same worker | Throughput. Each request now forks a new worker |
; /etc/php.ini or /etc/php/*/fpm/php.ini until you can patch session.upload_progress.enabled = 0 fatal_error_backtraces = 0 ; PHP 8.5 only
Do not use RLIMIT_AS or RLIMIT_DATA limits. The advisory warns that they make mmap() fail, which triggers the same vulnerable out-of-memory path.
How PHP2Shell works
Both flaws are about PHP trusting memory it should not trust after a request is cut short. Neither needs a vulnerable application: the bug is in the engine and in request startup.
CVE-2026-109618: remote code execution via out-of-memory corruption
When PHP runs out of memory it calls longjmp() and abandons whatever it was doing. During multipart body parsing, some request memory is reachable before it is fully initialised. An out-of-memory error at that moment leaves it half-built, and the request shutdown code then reads it as if it were valid. An attacker who shapes the request controls that memory.
Attackers can reach the memory limit while staying inside PHP's apparent limits: a multipart body without a usable declared length can slip past the first post_max_size check, a single part can carry unlimited headers, and empty file fields still build metadata arrays. The fix replaces longjmp() with a deferred soft limit plus a hard _exit().
CVE-2026-109619: remote code execution via stale global state
A PHP worker keeps global state (error, session, decoder and extension state) that can point into one request's memory. If a fatal error or memory exhaustion interrupts an operation before its reset runs, the request heap is freed while that global still points at it. The next request on the same worker can touch the stale pointer before normal initialisation replaces it. This is a use-after-free, and with a prepared heap it becomes code execution.
The advisory confirms two consumers that run before initialisation. The first is session upload progress, which is enabled by default and works without the application ever calling session_start(). The second is any INI diagnostic raised before request startup, for example a deprecated value passed through Apache php_value, .htaccess, or a frontend's FastCGI PHP_VALUE parameter. Silencing errors with error_reporting, @ or display_errors does not help, because PHP touches the stale state before it decides whether to report anything.
Which PHP setups are exposed
| How PHP runs | CVE-2026-109618 | CVE-2026-109619 |
|---|---|---|
| Apache mod_php | Exploited. Fires before application code | Exploited. Confirmed RCE |
| PHP-FPM (NGINX, Apache proxy_fcgi) | Susceptible. The failed-startup path is blocked because FPM kills the child, but out-of-memory errors later in a request remain | Exploited. Upload progress RCE validated against FPM |
| FrankenPHP | Likely, like mod_php | Conditional |
| LiteSpeed (LSAPI) | Likely, like FPM | Conditional |
Source: the PHP security team's analysis in the two advisories. "Conditional" means exploitable when the application or configuration supplies the missing step, not that the setup is safe.
Detect PHP2Shell exploitation attempts
The exploit is noisy before it is reliable: attackers usually need several requests to line up the heap, and failed guesses crash workers. Look for these traces.
Out-of-memory errors with no script location
grep -rh 'Allowed memory size of' /var/log/php*-fpm/ /var/log/php-fpm/ /var/log/httpd/ /var/log/apache2/ 2>/dev/null \ | grep 'Unknown on line 0' | tail -n 20
PHP workers dying on segmentation faults
journalctl --since '2026-10-17' | grep -E 'php.*(SIGSEGV|signal 11|Segmentation fault)' | tail -n 20 grep -rh 'exited on signal 11' /var/log/php*-fpm* /var/log/php-fpm/ 2>/dev/null | tail -n 20
Crashing workers seen from NGINX
When a PHP-FPM child dies mid-request, NGINX logs it and returns 502. A burst of 502s on POSTs from one address is a strong signal:
grep -E 'upstream prematurely closed|Connection reset by peer\) while reading response header' /var/log/nginx/error.log | tail -n 20
awk '$6 == "\"POST" && $9 == 502 {print $1}' /var/log/nginx/access.log | sort | uniq -c | sort -rn | head
The public proof of concept sends chunked multipart POSTs with megabyte-sized field names and a PHP_SESSION_UPLOAD_PROGRESS field. Access logs do not record request size by default. Add $request_length to your log_format to spot oversized uploads in the future.
Shells spawned by the web server
ps -eo user,pid,ppid,etime,cmd --forest | grep -E -B1 '(sh|bash|python|perl|nc|curl|wget) ' | grep -E -A1 'php|httpd|apache'
Also look for outbound connections owned by the PHP user, new files in upload and cache directories, and strings like /dev/tcp/, fsockopen or mkfifo in recently changed PHP files. Clean logs do not prove a host is safe. If you cannot rule out a compromise, rotate its credentials and rebuild it.
Watching many servers? GetPageSpeed Amplify lists the PHP-FPM runtime on every monitored host and marks it patched, unverified or vulnerable against PHP2Shell. When a build is provably vulnerable, it emails you the exact upgrade commands.
PHP2Shell timeline
- Amazon's MadPot honeypots record an exploitation attempt, four days before disclosure.
- Details leak publicly and the PHP project announces an emergency security release for the next day.
- PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 ship with the two advisories. Source-only backports appear for 7.4, 8.0 and 8.1. Researchers publish technical write-ups, and CDN vendors roll out WAF rules.
- GetPageSpeed publishes patched PHP 5.6, 7.0, 7.3, 7.4 and 8.0 builds for CentOS 7. Public scanners appear, and attacks spread.
- This page last updated.
Need hands-on help?
Stuck on an old platform, a fleet you cannot patch by hand, or a server you suspect is compromised? GetPageSpeed engineers patch, harden and monitor production PHP and NGINX servers.
See Server CarePHP2Shell FAQ
What is PHP2Shell?
PHP2Shell is the name for two critical PHP vulnerabilities disclosed on October 21, 2026: CVE-2026-109618 (remote code execution via out-of-memory corruption) and CVE-2026-109619 (remote code execution via stale global state). Both are rated CVSS 4.0 9.2 and need no authentication. They are bugs in PHP itself, so every application on an affected PHP build is exposed, including WordPress, Drupal, Magento and Laravel sites.
Which PHP versions are affected by PHP2Shell?
Every PHP branch from 5.4 to 8.5 that serves web requests. Upstream fixes exist only in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. PHP 8.1 and older received no upstream release, so they are protected only by a vendor backport, such as a Debian, Ubuntu or RHEL security update, or the GetPageSpeed builds for CentOS 7.
Is PHP-FPM affected, or only mod_php?
Both. Apache mod_php is the worst case: the confirmed exploit fires during request startup, before any application code runs. PHP-FPM kills a worker whose startup fails, which blocks that exact sequence, but the PHP security team validated session upload progress RCE against FPM as well. FrankenPHP, php-cgi and LiteSpeed (LSAPI) are also susceptible. Patch every SAPI.
Does session.upload_progress.enabled=0 fix PHP2Shell?
No. It removes one confirmed path for CVE-2026-109619 and does nothing for CVE-2026-109618. The PHP security team says updating to a fixed release is the only complete mitigation. Keep upload progress disabled anyway if you do not use it.
Why does php -v still show the old version after I patched?
Distributions backport security fixes into the version they already ship. Ubuntu 24.04, for example, keeps reporting PHP 8.3.6 after the fix, and Debian, RHEL and the GetPageSpeed CentOS 7 builds keep their upstream version numbers too. Check the package release instead: paste rpm -qa or dpkg -l output into the checker on this page.
Is there a PHP2Shell fix for PHP 7.4, 8.0 or 8.1?
Not from php.net, which published only source backports for those branches. Fixed packages come from vendors: your distribution's security updates, Ondřej Surý's packages, cPanel EasyApache, Plesk, CloudLinux, and for CentOS 7 the free GetPageSpeed channels for PHP 5.6, 7.0, 7.3, 7.4 and 8.0.
How do I fix PHP2Shell on CentOS 7?
Install the GetPageSpeed release package with yum install https://extras.getpagespeed.com/release-latest.rpm, then upgrade PHP from the channel for your version, for example yum --enablerepo=getpagespeed-extras-php74 upgrade 'php-*', or yum upgrade 'php74-php*' for the Remi software collection, and restart PHP-FPM. The channels are free and need no subscription.
Is WordPress affected by PHP2Shell?
Yes, if it runs on a vulnerable PHP build. The flaws sit in the PHP engine, not in WordPress code, so no plugin or WordPress update fixes them. Update PHP itself, then restart PHP-FPM or Apache.
Is a WAF such as Cloudflare enough?
No. Cloudflare and other vendors deployed rules for the known exploit shapes, and they reduce noise, but attackers mutate payloads and traffic that reaches the origin directly bypasses the CDN. Treat a WAF as a stopgap while you patch.
Do I need to restart PHP after updating?
Yes. Running PHP-FPM workers, Apache children and LiteSpeed processes keep the old code in memory until they restart. Restart php-fpm, httpd or apache2, and lsphp after the upgrade, then check again.
How can I tell whether my server was already exploited?
Look for memory-exhaustion errors reported at "Unknown on line 0", PHP-FPM children exiting on signal 11 (segmentation fault), multipart POSTs with huge field names or a PHP_SESSION_UPLOAD_PROGRESS field, and shell processes spawned by the web server user. Clean logs are not proof: rotate credentials and rebuild hosts you cannot rule out.
References
- GHSA-w8hw-w9fx-j79w: CVE-2026-109618, RCE via OOM corruption (PHP security team)
- GHSA-6wmf-4rv5-p3rp: CVE-2026-109619, RCE via stale global state (PHP security team)
- CVE-2026-109618 and CVE-2026-109619 on cve.org
- PHP release announcements: 8.5.12, 8.4.27, 8.3.36, 8.2.35
- SANS Internet Storm Center diary on the public proof of concept
- Datadog Security Labs on detection signals
- PHP2Shell on CentOS 7: patched PHP 5.6, 7.0, 7.3, 7.4 and 8.0 (GetPageSpeed)