Guide

PHP2Shell (CVE-2026-109618, CVE-2026-109619): check and patch your PHP

By Aman Singh · Published · 11 min read

On 21 October 2026 the PHP project released fixes for two critical vulnerabilities, CVE-2026-109618 and CVE-2026-109619, nicknamed PHP2Shell. Both are rated 9.2 (CVSS v4.0) and can let an unauthenticated attacker run code on a web server. Almost every PHP install is affected, so the question for most teams is not whether, but where: which servers, containers and hosting accounts still run an unpatched PHP.

This guide gives the fixed versions, a checker script that tells you whether a server is patched (including distribution builds that keep the old version number), patch steps for common setups, and the temporary settings to use if you cannot update today. Facts come from PHP's own advisories; the script was run on official PHP 7.4.33, 8.0.30, 8.4.26 and 8.4.27 builds on 22 October 2026.

On shared hosting, XAMPP or an old PHP version? Have PHP patched for you →

Every path ends the same way: restart the PHP workers, then run the checker through the web server, not just the command line.

What PHP2Shell is

CVE-2026-109618CVE-2026-109619
PHP's titleRCE via out-of-memory corruptionRCE via stale global state
What goes wrongAn out-of-memory error aborts a request while internal request data is only partly set up; request shutdown then uses invalid memoryState owned by one request survives after its memory is freed; a later request on the same worker uses it before normal start-up
SeverityCVSS v4.0 9.2, no authenticationCVSS v4.0 9.2, no authentication
Apache mod_phpRemote code execution demonstrated over HTTPRemote code execution confirmed
PHP-FPMConsidered susceptible; no complete exploit shownExposed depending on configuration; exploitation validated

Researchers also showed that worker memory can be leaked to an unauthenticated attacker, which can expose passwords, tokens and API keys held in memory. Reports on real attacks differ: Qualys cites reports of exploitation from Amazon Threat Intelligence, Cloudflare and Patchstack, while Huntress said on 21 October it had not yet seen exploitation on its partners' systems. Treat it as urgent either way.

Fixed versions

BranchFixed upstream releaseStatus of the branch
PHP 8.58.5.12Supported
PHP 8.48.4.27Supported
PHP 8.38.3.36Supported
PHP 8.28.2.35Security fixes end 31 December 2026
PHP 8.1, 8.0, 7.4None (source backports only)End of life
PHP 8.6 pre-releases8.6.0 RC4 and laterNot for production

Distribution packages that include the fix, as listed by Ubuntu and Debian on 21 October:

DistributionPackageFixed in
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 (Ubuntu Pro)php7.47.4.3-4ubuntu2.29+esm5
Debian 13 (trixie)php8.48.4.27-1~deb13u1
Debian 12 (bookworm, LTS)php8.28.2.35-1~deb12u1

Step 1: find every PHP you run

Servers often have several PHP versions side by side, and the one on the command line is not necessarily the one behind your website. List them all first:

Terminal (Linux)
# Every PHP binary and version the CLI sees
php -v
ls /etc/php/ 2>/dev/null            # Debian/Ubuntu: one folder per installed branch

# PHP-FPM services (often a different version from the CLI)
systemctl list-units --type=service 'php*fpm*'

# Apache with mod_php (highest risk)
apachectl -M 2>/dev/null | grep -i php

# Containers: which PHP image each one runs
docker ps --format '{{.Names}}  {{.Image}}'

Do not forget staging servers, old subdomains, admin tools, containers built months ago, and hosting accounts that only run a WordPress site.

Step 2: check each one with a script

This script reads the running PHP version, compares it with the fixed releases (and the known fixed Ubuntu builds), and shows which interim mitigations are active. It runs on PHP 7.0 and later, so it also works on the old servers that need it most.

The script and its tests are also on GitHub as AmanSin001/php2shell-check (MIT licence). Download it with curl -O https://raw.githubusercontent.com/AmanSin001/php2shell-check/main/php2shell-check.php, or copy it from below.

php2shell-check.php
<?php
// php2shell-check.php: is this PHP patched for CVE-2026-109618 and
// CVE-2026-109619 ("PHP2Shell"), and which interim mitigations are on?
//
// Run it with the SAME PHP that serves your site. The CLI binary is often a
// different version from PHP-FPM or mod_php, so also run it through the web
// server (upload it under a random name, open it once, then delete it).
// Works on PHP 7.0 and later.
//
// Exit codes (CLI): 0 = patched, 1 = vulnerable, 2 = check with your vendor.

const PHP2SHELL_FIXED = array(
    '8.2' => '8.2.35',
    '8.3' => '8.3.36',
    '8.4' => '8.4.27',
    '8.5' => '8.5.12',
);

// Distribution builds that keep an old upstream number but include the fix
// (Ubuntu security tracker, 21 October 2026). Key: upstream version.
const PHP2SHELL_DISTRO_FIXED = array(
    '8.5.4' => '0ubuntu1.5',          // Ubuntu 26.04
    '8.3.6' => '0ubuntu0.24.04.13',   // Ubuntu 24.04
    '8.1.2' => '1ubuntu2.28',         // Ubuntu 22.04
    '7.4.3' => '4ubuntu2.29+esm5',    // Ubuntu 20.04 (Ubuntu Pro)
);

/**
 * Decide the status from the version alone. $extra is PHP_EXTRA_VERSION:
 * distribution builds often set it, and can backport fixes without changing
 * the upstream version number.
 */
function php2shell_status($version, $extra = '')
{
    if (!preg_match('/^(\d+)\.(\d+)\.(\d+)(.*)$/', $version, $m)) {
        return array('check-vendor', "Unrecognised version string: $version");
    }
    $branch = "$m[1].$m[2]";
    $plain = "$m[1].$m[2].$m[3]";
    $suffix = $m[4] . $extra;

    $revision = ltrim($extra, '-');
    if ($revision !== '' && isset(PHP2SHELL_DISTRO_FIXED[$plain]) && strpos($revision, 'ubuntu') !== false) {
        $fixedRevision = PHP2SHELL_DISTRO_FIXED[$plain];
        return version_compare($revision, $fixedRevision, '>=')
            ? array('patched', "Ubuntu build $plain-$revision includes the backported fix ($plain-$fixedRevision or later).")
            : array('vulnerable', "Ubuntu build $plain-$revision is older than the fixed package $plain-$fixedRevision. Run apt upgrade.");
    }

    if (version_compare($branch, '8.6', '>=')) {
        // 8.6.0 RC4 and later contain the fixes; earlier pre-releases do not.
        if (preg_match('/alpha|beta|RC[1-3]\b/i', $suffix)) {
            return array('vulnerable', "$version is a pre-release without the fixes. Use 8.6.0 RC4 or later, and keep pre-releases out of production.");
        }
        return array('patched', "$version includes the fixes (8.6.0 RC4 and later).");
    }

    if (!isset(PHP2SHELL_FIXED[$branch])) {
        $note = "PHP $branch is end of life, so there is no fixed upstream release. The PHP team published source backports for 7.4, 8.0 and 8.1 only.";
        if ($extra !== '') {
            return array('check-vendor', "$note This is a vendor build ($version$extra): confirm with your vendor that it includes the backport, or upgrade to a supported branch.");
        }
        return array('vulnerable', "$note Upgrade to 8.4.27 or 8.5.12, or rebuild with the official backport.");
    }

    $fixed = PHP2SHELL_FIXED[$branch];
    if (version_compare($plain, $fixed, '>=')) {
        return array('patched', "$plain is at or above the fixed release $fixed.");
    }
    if ($extra !== '') {
        return array('check-vendor', "Upstream $plain is below $fixed, but this is a vendor build ($version$extra). Distributions backport fixes without changing the version: check the package changelog for CVE-2026-109618 and CVE-2026-109619.");
    }
    return array('vulnerable', "$plain is below the fixed release $fixed. Update now.");
}

function php2shell_bytes($value)
{
    $value = trim((string) $value);
    if ($value === '' || $value === '-1') {
        return -1;
    }
    $n = (int) $value;
    $unit = strtolower(substr($value, -1));
    if ($unit === 'g') {
        return $n * 1073741824;
    }
    if ($unit === 'm') {
        return $n * 1048576;
    }
    if ($unit === 'k') {
        return $n * 1024;
    }
    return $n;
}

/** Interim settings from the PHP advisories. They reduce risk; they do not fix it. */
function php2shell_mitigations()
{
    $memory = (string) ini_get('memory_limit');
    $post = (string) ini_get('post_max_size');
    $postBytes = php2shell_bytes($post);
    $progress = ini_get('session.upload_progress.enabled') ? '1' : '0';

    $rows = array(
        array('memory_limit', $memory, $memory === '-1', 'Set to -1 with php_admin_value until patched; restore a real limit afterwards.'),
        array('post_max_size', $post, $postBytes > 0 && $postBytes <= 33554432, 'Keep at 8M, or 32M if you need uploads.'),
        array('session.upload_progress.enabled', $progress, $progress === '0', 'Set to 0 in php.ini or the FPM pool (not ini_set or .user.ini), then restart workers.'),
    );
    if (PHP_VERSION_ID >= 80500) {
        $backtraces = (string) ini_get('fatal_error_backtraces');
        $rows[] = array('fatal_error_backtraces', $backtraces, $backtraces === '0' || $backtraces === '', 'PHP 8.5: set to 0 and restart workers.');
    }
    return $rows;
}

function php2shell_report()
{
    list($status, $reason) = php2shell_status(PHP_VERSION, PHP_EXTRA_VERSION);
    $lines = array(
        'PHP2Shell check (CVE-2026-109618, CVE-2026-109619)',
        'PHP version:  ' . PHP_VERSION . PHP_EXTRA_VERSION,
        'SAPI:         ' . PHP_SAPI . (PHP_SAPI === 'apache2handler' ? '  (mod_php: highest risk, patch first)' : ''),
        'Status:       ' . strtoupper($status),
        'Why:          ' . $reason,
        '',
        'Interim mitigations (temporary, not a fix):',
    );
    foreach (php2shell_mitigations() as $row) {
        list($name, $value, $on, $advice) = $row;
        $lines[] = sprintf('  [%s] %s = %s  %s', $on ? 'x' : ' ', $name, $value === '' ? '(empty)' : $value, $advice);
    }
    $lines[] = '';
    $lines[] = 'Linux packages can include the fix without a new version number: also compare';
    $lines[] = 'your package version (dpkg -l "php*" or rpm -qa "php*") with your distribution\'s advisory.';
    if (PHP_SAPI === 'cli') {
        $lines[] = 'This was the CLI. Run it through your web server too: FPM and mod_php can be a different PHP.';
    }
    return array($status, implode(PHP_EOL, $lines) . PHP_EOL);
}

// Only produce output when run directly, so tests can include this file.
if (isset($_SERVER['SCRIPT_FILENAME']) && realpath($_SERVER['SCRIPT_FILENAME']) === __FILE__) {
    list($status, $report) = php2shell_report();
    if (PHP_SAPI !== 'cli') {
        header('Content-Type: text/plain; charset=utf-8');
        header('Cache-Control: no-store');
        header('X-Robots-Tag: noindex');
    }
    echo $report;
    if (PHP_SAPI === 'cli') {
        $codes = array('patched' => 0, 'vulnerable' => 1, 'check-vendor' => 2);
        exit($codes[$status]);
    }
}
Terminal
# 1. With the CLI
php php2shell-check.php; echo "exit code: $?"

# 2. Through the web server, which is what attackers reach
cp php2shell-check.php /var/www/html/check-$(openssl rand -hex 6).php
#    open that URL once in your browser, then delete the file

Output on an unpatched PHP 8.4 build:

Output
PHP2Shell check (CVE-2026-109618, CVE-2026-109619)
PHP version:  8.4.26
SAPI:         cli
Status:       VULNERABLE
Why:          8.4.26 is below the fixed release 8.4.27. Update now.

Interim mitigations (temporary, not a fix):
  [ ] memory_limit = 128M  Set to -1 with php_admin_value until patched; restore a real limit afterwards.
  [x] post_max_size = 8M  Keep at 8M, or 32M if you need uploads.
  [ ] session.upload_progress.enabled = 1  Set to 0 in php.ini or the FPM pool (not ini_set or .user.ini), then restart workers.

Results from the test runs:

Tested on 22 October 2026
PHP 8.4.27 (patched build)    Status: PATCHED      exit 0
PHP 8.4.26 (one release older) Status: VULNERABLE   exit 1
PHP 8.0.30 (XAMPP, end of life) Status: VULNERABLE  exit 1
PHP 7.4.33 (end of life)        Status: VULNERABLE  exit 1
Through PHP's web server:       SAPI cli-server, text/plain, X-Robots-Tag: noindex
Unit tests (7.4, 8.0 and 8.4):  all 24 passed
Show the unit tests (24 version cases)
php2shell-check.test.php
<?php
// Tests the version rules without needing every PHP build installed.
require __DIR__ . '/php2shell-check.php';

$cases = array(
    // version, PHP_EXTRA_VERSION, expected status
    array('8.5.12', '', 'patched'),
    array('8.5.11', '', 'vulnerable'),
    array('8.4.27', '', 'patched'),
    array('8.4.26', '', 'vulnerable'),
    array('8.3.36', '', 'patched'),
    array('8.3.35', '', 'vulnerable'),
    array('8.2.35', '', 'patched'),
    array('8.2.34', '', 'vulnerable'),
    // Ubuntu backports keep the old upstream number
    array('8.3.6', '-0ubuntu0.24.04.13', 'patched'),
    array('8.3.6', '-0ubuntu0.24.04.14', 'patched'),
    array('8.3.6', '-0ubuntu0.24.04.9', 'vulnerable'),
    array('8.1.2', '-1ubuntu2.28', 'patched'),
    array('8.1.2', '-1ubuntu2.27', 'vulnerable'),
    array('7.4.3', '-4ubuntu2.29+esm5', 'patched'),
    array('7.4.3', '-4ubuntu2.29+esm4', 'vulnerable'),
    array('8.5.4', '-0ubuntu1.5', 'patched'),
    // other vendor builds below the fixed release: ask the vendor
    array('8.3.20', '-1+deb.sury.org+1', 'check-vendor'),
    array('8.1.33', '', 'vulnerable'),
    array('7.4.33', '', 'vulnerable'),
    array('7.4.33', '-1+deb11u9', 'check-vendor'),
    array('5.6.40', '', 'vulnerable'),
    array('8.6.0RC3', '', 'vulnerable'),
    array('8.6.0RC4', '', 'patched'),
    array('8.6.0', '', 'patched'),
);

$failed = 0;
foreach ($cases as $case) {
    list($version, $extra, $expected) = $case;
    list($actual) = php2shell_status($version, $extra);
    $ok = $actual === $expected;
    $failed += $ok ? 0 : 1;
    printf("%s %-10s %-20s %s\n", $ok ? 'ok  ' : 'FAIL', $version, $extra === '' ? '' : $extra, $actual);
}
echo $failed === 0 ? "all " . count($cases) . " passed\n" : "$failed failed\n";
exit($failed === 0 ? 0 : 1);

Step 3: patch it, by setup

  1. 1
    Ubuntu and Debian packages. Install the security update, then confirm the package version against the table above:
    Terminal
    sudo apt update
    sudo apt install --only-upgrade 'php*'
    dpkg -l 'php8*' | grep '^ii'       # compare with the fixed versions below
    sudo systemctl restart php8.3-fpm  # use your branch; restart apache2 for mod_php
    If you use a third-party repository such as the Ondřej Surý PPA, update from it and confirm its PHP version reaches the fixed release.
  2. 2
    Red Hat, Rocky, AlmaLinux and others. Run dnf upgrade 'php*' once your vendor publishes fixed packages, and confirm with the vendor advisory: backports there can also keep the old version number.
  3. 3
    Docker. The official images were rebuilt on 21 October. Pin the fixed release and rebuild every image that uses PHP, then redeploy. Pulling a newer image does nothing for containers that are already running.
    Dockerfile
    # Dockerfile: pin the patched release, then rebuild and redeploy
    FROM php:8.4.27-fpm
  4. 4
    Shared and managed hosting, cPanel, Plesk, WordPress hosts. The host updates PHP for you, but timing varies. Select a supported PHP version in your control panel if you are on an old one, then run the checker through the web to confirm what your site actually uses.
  5. 5
    Windows and XAMPP. XAMPP bundles its own PHP, and many installs still ship end-of-life versions. Replace it with a supported build from windows.php.net, or keep XAMPP off the public internet.

Step 4: restart workers and verify

Updating the files on disk does not change PHP processes that are already running. Restart PHP-FPM (for example systemctl restart php8.3-fpm) or Apache for mod_php, restart or replace containers, then run the checker through the web server again. The status should read PATCHED. If you set any temporary settings, remove them now and put back a normal memory_limit.

If you cannot patch today

PHP's advisories list settings that reduce exposure to specific attack paths. They do not make a server safe; plan the update anyway. Apply them at server level, because ini_set() and .user.ini do not work for these, and restart every worker afterwards.

php.ini (tested: loads and shows as active in the checker)
; TEMPORARY PHP2Shell mitigations (CVE-2026-109618, CVE-2026-109619).
; They reduce exposure; only updating PHP fixes the flaws. Remove after patching.
memory_limit = -1
post_max_size = 8M
upload_max_filesize = 6M
session.upload_progress.enabled = 0
; PHP 8.5 only:
; fatal_error_backtraces = 0
PHP-FPM pool
; /etc/php/8.x/fpm/pool.d/www.conf  (temporary, until patched)
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M
php_admin_flag[session.upload_progress.enabled] = off
Apache with mod_php
# Apache with mod_php: virtual host or server config (temporary)
php_admin_value memory_limit -1
php_admin_value post_max_size 8M
php_admin_flag session.upload_progress.enabled off
  • Do not set RLIMIT_AS or RLIMIT_DATA to limit PHP memory: the advisory warns they can trigger the vulnerable path. Use container or cgroup memory limits instead.
  • Limit request size at the proxy or web server too, including requests without a usable Content-Length.
  • Restrict access to anything that does not need to be public, or take it offline until it is patched.
  • The FPM and Apache lines follow PHP's configuration syntax; only the php.ini block was run in the tests above.

Running PHP 8.1 or older

PHP 8.1, 8.0, 7.4 and older get no new upstream releases. The PHP team published minimal source patches for 7.4, 8.0 and 8.1 for people who build PHP themselves, and some distributions and paid long-term support vendors ship fixed packages (Ubuntu 22.04 and 20.04 Pro are in the table above). Otherwise the fix is to move to PHP 8.4 or 8.5. PHP 8.2 also stops receiving security fixes on 31 December 2026, so if you are upgrading anyway, skip it.

After patching: look for signs

  • Unusual multipart POST requests followed by out-of-memory errors or bursts of HTTP 500 responses in the web server log.
  • Unexpectedly long or malformed PHPSESSID cookies in responses.
  • PHP workers crashing or restarting more than usual.
  • Files in the web root you did not deploy, new admin users, or new cron jobs.

Because memory disclosure was demonstrated, rotate secrets that PHP holds in memory (database passwords, API keys, session signing keys) if a server was internet-facing and unpatched, especially with mod_php. If you find signs of compromise, rebuild the server rather than cleaning it.

Common questions

Which PHP versions fix CVE-2026-109618 and CVE-2026-109619?+
PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35, released on 21 October 2026, and 8.6.0 RC4 for testers. Distribution packages may fix it without changing the version number, so check your vendor's advisory.
Is WordPress affected by PHP2Shell?+
The flaws are in PHP itself, not in WordPress, so any WordPress site is affected if its server runs an unpatched PHP. Updating WordPress or plugins does not fix it; the PHP version your host runs has to be updated.
Is PHP-FPM safer than mod_php here?+
Apache mod_php is the highest risk: PHP demonstrated attacks that work before any application code runs. PHP-FPM is also exposed depending on configuration, so it needs the update too.
How do I check my PHP version on Linux?+
Run php -v for the command-line PHP, and list PHP-FPM services with systemctl list-units 'php*fpm*'. The web server can use a different version, so also run a small PHP file through the web server, such as the checker in this guide.
Do the php.ini mitigations fix the vulnerability?+
No. PHP's advisories say updating is the only complete fix. The settings reduce exposure to specific attack paths while you schedule the update.
I run PHP 7.4. What should I do?+
Upgrade to PHP 8.4 or 8.5 if you can. If you cannot yet, use a distribution or long-term support package that includes the backport (for example Ubuntu 20.04 with Ubuntu Pro), or build from the PHP team's source backport, and keep the server off the public internet until it is done.

Want it patched for you today?

I check every PHP install you run, apply the update or the safest interim settings, restart and verify each one, and send a short report of what was found. See PHP security update and upgrade, or get in touch if it is urgent.

Sources

Next steps

Keep reading

Need a hand with this?

Tell me what you are working on. Free scoping call, fixed quote within 48 hours.

Book a call

← All guides