Skip to main content

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.

Sources: GHSA-w8hw-w9fx-j79w and GHSA-6wmf-4rv5-p3rp from the PHP security team.

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.

RHEL, CentOS, Alma, Rocky: rpm -qa 'php*'
Debian, Ubuntu: dpkg -l 'php*'
Alpine: apk info -v | grep php. Anything else: php -v

The check runs in your browser. Nothing you paste is sent anywhere.

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.

ReleasePackageFirst fixed versionAdvisory
Debian 13 trixiephp8.48.4.27-1~deb13u1DSA-6574-1
Debian 12 bookwormphp8.28.2.35-1~deb12u1DLA-4848-1
Ubuntu 26.10php8.58.5.9-0ubuntu2.1USN-8948-1
Ubuntu 26.04 LTSphp8.58.5.4-0ubuntu1.5
Ubuntu 24.04 LTSphp8.38.3.6-0ubuntu0.24.04.13
Ubuntu 22.04 LTSphp8.18.1.2-1ubuntu2.28
Ubuntu 20.04 (Pro)php7.47.4.3-4ubuntu2.29+esm5Ubuntu 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.

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.

GetPageSpeed CentOS 7 PHP builds with the PHP2Shell fix
PHPPatched buildWhere it comes from
5.65.6.40-43.el7PHP 5.6 adaptation of the upstream fix
7.07.0.33-45.el7port of the official 7.4 fix
7.37.3.33-25.el7official 7.4 fix, hunk for hunk
7.47.4.33-33.el7official 7.4 fix
8.08.0.30-24.el7official 8.0 backport
8.48.4.27-2.el7upstream 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 downgrade takes you back to Remi.
  • One behaviour change. A request that hits memory_limit now 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.

SettingWhat it blocksWhat it leaves open
session.upload_progress.enabled = 0The session upload progress path of CVE-2026-109619, confirmed against mod_php and FPMEvery 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 exploitOther error, session and extension state
memory_limit = -1The predictable, controllable out-of-memory trigger for CVE-2026-109618Real 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_varsCheaper paths to memory exhaustionApplication-specific memory exhaustion
pm.max_requests = 1 (FPM)State carried from one request to the next on the same workerThroughput. 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-109618 sequence A multipart request reaches the memory limit during parsing, PHP jumps out with longjmp leaving half-initialised memory, and request shutdown then uses that memory, giving code execution. Crafted multipart many headers, no length Out of memory mid-initialisation longjmp() exit memory left half-built Shutdown uses it attacker gets RCE With mod_php this all happens before your application code runs.
CVE-2026-109618: memory exhaustion during request startup.

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.

CVE-2026-109619 sequence Request A publishes state into a worker global and is killed by a fatal error before the reset; its memory is freed; request B on the same worker reads the stale pointer during upload progress or an INI diagnostic, before initialisation. One PHP worker process Request A fatal error before reset Heap freed global still points at it Request B upload progress reads it pm.max_requests = 1 breaks the chain, at a heavy performance cost.
CVE-2026-109619: state from one request is used by the next.

Which PHP setups are exposed

How PHP runsCVE-2026-109618CVE-2026-109619
Apache mod_phpExploited. Fires before application codeExploited. 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 remainExploited. Upload progress RCE validated against FPM
FrankenPHPLikely, like mod_phpConditional
LiteSpeed (LSAPI)Likely, like FPMConditional

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

  1. Amazon's MadPot honeypots record an exploitation attempt, four days before disclosure.
  2. Details leak publicly and the PHP project announces an emergency security release for the next day.
  3. 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.
  4. 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.
  5. 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 Care

PHP2Shell 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