How to Check and Patch PHP2Shell (CVE-2026-109618, CVE-2026-109619) on Your PHP Servers
If you run anything on PHP that serves web requests, upgrade today to PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35 (or your vendor’s patched package). On October 21, 2026 the PHP project shipped fixes for two critical bugs in the PHP engine itself, CVE-2026-109618 and CVE-2026-109619, which can allow unauthenticated remote code execution no matter what application runs on top. Wiz Research, which found them alongside Calif and OpenAI, calls the issue PHP2Shell and showed working pre-auth RCE against the stock WordPress, Drupal and Joomla container images. This guide walks through the practical part: figuring out which PHP build actually serves your traffic, checking it with OpenAI’s free php2shell-checker, upgrading on the common setups, and what to do if you truly cannot patch yet.
Everything below comes from the PHP security advisory on php.net, the Wiz Research write-up, OpenAI’s Patch the Planet disclosure post, and the checker’s README and source. We downloaded the checker script on October 22, 2026 (Beijing time) and ran it ourselves; output shown below is real. We did not run any exploit, and you shouldn’t either.
What PHP2Shell is, in one minute
Both CVEs come from the same root problem: PHP doesn’t always clean up its internal state when a request dies from a fatal error, especially an out-of-memory error. The next request handled by the same worker can then reuse freed memory as if it were still valid. According to the PHP advisory:
- CVE-2026-109618 (GHSA-w8hw-w9fx-j79w), “RCE via OOM corruption”: out-of-memory handling can interrupt PHP while internal request data is only partly initialized, and cleanup later treats that data as valid.
- CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp), “RCE via stale global state”: request-owned state survives after its memory is freed, and a later request on the same worker can use it before it is reset.
PHP’s pre-release notice rated both CVSS v4.0 9.2. Wiz’s table breaks the CVEs into four concrete bugs (an upload-progress unserializer use-after-free, a remote process-memory disclosure through the PHPSESSID cookie, a fatal-error backtrace use-after-free, and an OPcache type confusion) and notes that other variants exist under the same two CVEs. The memory-disclosure bug can leak passwords, tokens and API keys from other requests even without full code execution.
Three facts drive the urgency:
- It’s in PHP, not your app. Wiz writes that “even a simple ‘Hello World’ application running on an affected PHP version can be vulnerable.” Patching WordPress plugins does nothing here.
- It’s old. Wiz says every PHP version released in the last decade is affected by at least one of the bugs, going back to PHP 5.4.
- Every web SAPI is in scope. PHP lists PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed, and says it considers the risk highest for mod_php, where an unauthenticated request can trigger the attack before any application code runs.
PHP’s own wording on workarounds is blunt: “There are no complete mitigations, updating is the only safe choice.”
Step 1: Know your target versions
These are the fixed upstream releases from the PHP advisory:
| Branch | Fixed release | Notes |
|---|---|---|
| 8.5 | 8.5.12 | Also see the fatal_error_backtraces stopgap below |
| 8.4 | 8.4.27 | |
| 8.3 | 8.3.36 | |
| 8.2 | 8.2.35 | PHP 8.2 reaches end of life on December 31, 2026 |
| 8.1 and older | none from php.net | Minimal source backports were published for 7.4, 8.0 and 8.1; otherwise use a vendor/LTS backport or upgrade |
| 8.6.0 (pre-release) | RC4 | Contains the fixes; not for production |
Linux distributions and LTS vendors got the patches in advance, so your distro may already have packages. The catch: a distro backport often keeps the old upstream version number (for example a Debian or Ubuntu 8.3.x package with a new package revision). That matters for Step 3.
Step 2: Find the PHP that actually serves your site
The most common mistake in a response like this is checking the wrong binary. php -v reports the CLI build on your PATH, which can differ from the PHP-FPM pool or Apache module that handles web traffic. OpenAI’s server-side checker skill says the same thing: start with php -v, “but verify that it matches the site’s PHP-FPM or Apache installation.”
Check each of these on the server:
# CLI build on the PATH
php -v
# PHP-FPM builds (the binary name varies by distro: php-fpm, php-fpm8.3, php-fpm83 ...)
ls /usr/sbin/php-fpm* 2>/dev/null
php-fpm -v 2>/dev/null
# Which PHP processes are actually running
ps -eo pid,cmd | grep -E '[p]hp-fpm|[p]hp-cgi|[f]rankenphp|[l]sphp'
# Is Apache loading mod_php? (highest-risk setup per the PHP advisory)
apachectl -M 2>/dev/null | grep -i php
If apachectl -M lists a php module, you’re on mod_php, the configuration PHP calls the highest risk. Put that host first.
For containers, check the image each service runs, not your laptop:
docker ps --format '{{.Names}}\t{{.Image}}'
docker exec <container-name> php -v
Many official CMS images (WordPress, Drupal, Joomla) bundle their own PHP. Wiz’s RCE demonstrations were run against wordpress:latest, drupal:latest and joomla:latest as they were at test time, so a container you pulled weeks ago is not safe just because it says “latest” in your compose file.
Step 3: Run OpenAI’s php2shell-checker
OpenAI released php2shell-checker, a small Node.js script (Apache-2.0) that compares a PHP version against known-vulnerable ranges. Its README says it works “without exploit requests” and needs no packages. It is also packaged as two Codex skills, phpcheck-url and phpcheck-server. If you already use Codex (see our Codex Security guide), the README’s install line is:
$skill-installer install both skills from https://github.com/openai/php2shell-checker
Then ask Codex $phpcheck-server check the PHP version serving my site on a box you have shell on, or $phpcheck-url check example.com when you only have a URL.
To run it without Codex, get the repo and call the script with Node:
git clone https://github.com/openai/php2shell-checker
cd php2shell-checker
node phpcheck.js --version # prints 1.0.0 at the time of writing
Check a URL
node phpcheck.js "https://example.com"
node phpcheck.js "https://example.com" --json --timeout 10
The script sends a HEAD request (falling back to GET only on 405 or 501), reads the Server and X-Powered-By headers, does not follow redirects, and looks for a PHP/x.y.z string. Against a site that hides its PHP version, which is most well-configured sites, you’ll see what we saw when we tried it on php.net:
UNKNOWN: PHP version not found
With --json:
{"patch_status":"unknown","php_version":null,"reason":"PHP version not found","exit_code":3}
UNKNOWN does not mean patched. The README says so explicitly. URL mode is only useful for quickly sweeping hosts that still leak X-Powered-By; for anything you own, use local mode.
Check a local binary
node phpcheck.js --local
node phpcheck.js --local --php /usr/sbin/php-fpm8.3 --json
--local runs php -v (or the binary you pass to --php) and checks the version it prints. Point --php at the FPM or CGI binary you found in Step 2, not just the CLI.
Reading the result
| Result | Exit code | Meaning (from the README) |
|---|---|---|
OK | 0 | Stable patch version above the embedded cutoff for a listed branch |
VULNERABLE | 1 | Version at or below the cutoff |
UNKNOWN | 3 | No version found, unrecognized branch, or a newer pre-release build |
| (error) | 2 | Invalid arguments |
The exit codes make it easy to loop over hosts in a script or CI job. The embedded cutoffs in version 1.0.0 (dated October 20, 2026) treat these as vulnerable, inclusive: 8.5.0–8.5.11, 8.4.0–8.4.26, 8.3.0–8.3.35, 8.2.0–8.2.34, 8.1.0–8.1.34, 8.0.0–8.0.30, every 7.x release through 7.4.33, and 5.4–5.6. Those line up with the fixed releases PHP published the next day.
Two limits to keep in mind, both stated in the README. First, OK is just a version comparison; it “does not independently verify a security fix.” Second, distribution backports may not change the upstream version, so a patched Debian, Ubuntu or RHEL package can show as VULNERABLE. In that case, check the package changelog instead:
# Debian / Ubuntu
dpkg -l | grep -E '^ii +php'
apt changelog php8.3-fpm | head -40
# RHEL / Rocky / Alma / Fedora
rpm -qa | grep -i php
rpm -q --changelog php-fpm | head -40
You’re looking for an entry that mentions CVE-2026-109618 and CVE-2026-109619. If your vendor hasn’t published one, treat the host as vulnerable.
Step 4: Upgrade
The general rule from PHP: update to 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or take your vendor’s package. After any upgrade, restart every PHP worker. A long-lived FPM master or Apache process keeps running the old code until it restarts.
Distro packages (Debian/Ubuntu/RHEL family)
# Debian / Ubuntu
sudo apt update
sudo apt install --only-upgrade 'php8.3*' # match your branch
sudo systemctl restart php8.3-fpm # or: sudo systemctl restart apache2
# RHEL family
sudo dnf upgrade 'php*'
sudo systemctl restart php-fpm # or: sudo systemctl restart httpd
Package and service names vary with how PHP was installed (distro repos, third-party PPAs, Remi, etc.), so check systemctl list-units | grep -i php for the right unit.
Docker images
The official php images on Docker Hub have tags for the fixed releases. We confirmed that php:8.5.12-fpm, php:8.4.27-apache, php:8.3.36-fpm and php:8.2.35-fpm exist. Pin the patch version in your Dockerfile instead of a floating minor tag so you can prove what you deployed:
FROM php:8.3.36-fpm
Then rebuild and redeploy without the cache, so the new base layer is actually used:
docker compose build --pull --no-cache
docker compose up -d
docker compose exec <service> php -v
For CMS images like wordpress, pull a fresh image and recreate the container (docker compose pull && docker compose up -d), then confirm the PHP version inside it with php -v.
Managed hosting and PaaS
If you don’t control the PHP binary (shared hosting, managed WordPress, a PaaS), check your provider’s status page or advisory. Wiz thanks Automattic, Akamai, AWS, Cloudflare, Fastly, Google, Imperva, Microsoft, Vercel and others for preparing guidance and “out-of-the-box mitigations” for customers who can’t patch immediately. Those edge mitigations are a stopgap, not a fix, and they only help traffic that actually goes through that provider.
PHP 8.1 and older
php.net no longer ships updates for 8.1 and below. The PHP security team did publish “minimal source-backports” for 7.4, 8.0 and 8.1 for people who build from source or have no LTS vendor. The sustainable answer is moving to a supported branch; note that 8.2 itself goes end of life on December 31, 2026, so 8.3 or newer is the better target.
Verify after upgrading
Re-run the checker against the binary that serves traffic, and confirm the version through the web path, not just the shell:
node phpcheck.js --local --php /usr/sbin/php-fpm8.3
If you can’t easily see the version served over HTTP, a short-lived phpinfo() page behind authentication (deleted right after) shows the SAPI and version the web server actually uses.
Step 5: If you can’t patch today
PHP lists interim measures that “reduce exposure to specific attack paths but do not make an installation safe.” Use them only to bridge the hours until you can upgrade.
For CVE-2026-109618 (OOM corruption):
- Set
memory_limit=-1in a way applications can’t override, such asphp_admin_valuein the FPM pool or Apache config, and make sure no pool, directory or app setting puts a finite limit back. For example, in an FPM pool file:php_admin_value[memory_limit] = -1. PHP notes an underlying allocation failure can still reach the bug. - Lower
post_max_size: PHP suggests8Mfor apps without uploads and32Mif you need uploads, withupload_max_filesizebelowpost_max_size. - Enforce request limits at the web server or proxy (decoded body size, input field count, multipart part count, header count, nesting depth), including for requests without a usable
Content-Length. - Use a container or cgroup memory limit as the availability boundary, ideally one that kills only the affected worker. Don’t use
RLIMIT_ASorRLIMIT_DATA, which PHP says can cause the vulnerable allocation failure.
For CVE-2026-109619 (stale global state):
- Set
session.upload_progress.enabled=0inphp.ini, an FPM pool or server config that applies before request parsing, then restart every worker. PHP says not to rely onini_set()or FPM.user.inifor this. It blocks the confirmed session paths but not other variants. - On PHP 8.5, set
fatal_error_backtraces=0and restart every worker. This blocks only the confirmed backtrace path. - Remove invalid or deprecated per-request settings that emit diagnostics before initialization. Hiding diagnostics with
error_reporting,display_errorsor@doesn’t help. - Fix fatal errors and execution timeouts that an attacker can trigger, including in callbacks, upload or import handlers, and cleanup code.
A combined FPM pool snippet for the two biggest levers looks like this (adjust to your pool file, then restart FPM):
; /etc/php/8.3/fpm/pool.d/www.conf (path varies by distro)
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M
php_admin_value[session.upload_progress.enabled] = 0
Check with php-fpm8.3 -tt (or your binary’s -tt flag) to confirm the pool parses before restarting, and remember these settings change app behavior: unlimited memory per request means a runaway script can now eat the container’s memory, which is why PHP pairs it with a cgroup limit.
Step 6: Look for signs you were already hit
Because the bugs run below your application, Wiz warns that application logs “may contain little context.” Until you’ve patched, it suggests watching for:
- Unusual multipart requests followed by memory-limit errors or repeated HTTP 500 responses
- Unexpectedly large or malformed
PHPSESSIDresponse cookies (the memory-disclosure bug leaks data through this cookie, even on apps that never callsession_start()) - Unexplained PHP worker churn
A rough first pass over web and PHP-FPM logs:
# Memory-limit fatals in PHP / FPM logs
sudo grep -i "allowed memory size" /var/log/php*-fpm.log /var/log/apache2/error.log 2>/dev/null | tail -50
# Bursts of 500s in an nginx/Apache combined access log
awk '$9 == 500 {print $1, $4, $7}' /var/log/nginx/access.log | tail -50
If you find a pattern that fits, treat the host as possibly compromised: the disclosure bug can leak secrets held in worker memory, so rotate database passwords, API keys and session secrets that the PHP process could read, the same way we recommended after the LiteLLM supply-chain compromise. OpenAI’s post also points operators to CISA guidance on prioritization and remediation and its Patch the Planet site for interim mitigations and backup and recovery advice.
Troubleshooting
The checker says VULNERABLE but apt says I’m up to date. Your distro may have backported the fix without changing the upstream version, or your mirror may not have the update yet. Check the package changelog for the two CVE IDs (Step 3). If it isn’t there, you’re not patched.
The checker says OK but I’m not sure the web server uses that binary. That’s the right instinct. Compare the FPM/CGI binary from ps with the one you checked, and confirm with a temporary authenticated phpinfo() page.
php -v shows the new version, but nothing changed. Restart every PHP worker (FPM, Apache with mod_php, LiteSpeed, FrankenPHP). PHP’s advisory repeats “restart every worker” for a reason.
URL mode returns UNKNOWN for all my sites. Expected if you set expose_php = Off or strip X-Powered-By at a proxy. Use --local on the server or the Codex phpcheck-server skill instead.
I’m on PHP 7.x and can’t upgrade this week. Look for an LTS vendor backport or build from PHP’s minimal 7.4 source backport, apply the Step 5 stopgaps, put the site behind a provider with a PHP2Shell mitigation, and schedule the move to 8.3 or newer.
FAQ
Do I need to update WordPress, Drupal or Joomla? Not for this bug. The flaw is in PHP itself, so the fix is a PHP update. Wiz used those CMSs only as demonstration targets.
Is the CLI affected? PHP’s advisory scopes the risk to installations “serving web requests.” Command-line scripts aren’t the focus, but update the CLI package too since it usually ships from the same source.
Who found it? Calif found a critical flaw using GPT-5.6 Sol as part of OpenAI’s Patch the Planet initiative, and Wiz Research independently found and reported both vulnerabilities, using Wiz Atlas, Gemini 4 Argon and GPT-5.6 Sol. PHP’s maintainers led the fix and release. Wiz’s disclosure timeline starts on August 19, 2026, when it found the first bug.
Why is this getting so much attention? Wiz compares it to Heartbleed and Log4Shell and says PHP powers about 70% of websites. OpenAI calls it “one of the most impactful security issues in recent memory” and argues that AI lowered the cost of turning old, hard-to-exploit memory bugs into working exploits. Either way, assume capable attackers can now build exploits quickly too.
Is there a public exploit? We haven’t checked, and we didn’t run one. You don’t need one to act: the fixed versions are out, and patching is the only complete fix.
For more on how AI-found vulnerabilities are landing in the software builders depend on, see our earlier piece on Pion DTLS advisories from Anthropic’s OSS scanner, or browse the latest on the 7minai homepage.