PHP2Shell and WordPress: Why `php -v` Is the Wrong Check
Run php -v on a fully patched Ubuntu 24.04 server this week and it prints PHP 8.3.6.
Every article published since Tuesday tells you the fixed version is 8.3.36. So the server that is correctly patched reads as thirty releases behind, and the obvious next step — add a third-party PHP repository and force an upgrade — churns a production stack for no security gain at all.
The inverse is worse, and it is the one that will actually hurt people. There are stacks where
apt install php8.3 completes, reports success, prints a patched version, and keeps serving the
old binary to every web request.
Both of those failures come from the same mistake: treating a version string as evidence.
What PHP2Shell actually is
On 21 October 2026 the PHP project shipped an out-of-cycle security release covering two vulnerabilities in the interpreter itself, now generally called PHP2Shell:
- CVE-2026-109618 — state corruption via bailout. When a request runs out of memory, PHP can be interrupted part-way through setting up its internal request data, and the cleanup path then treats that half-built data as valid.
- CVE-2026-109619 — a use-after-free across requests. A worker holds global fields for errors and session state that point into the current request’s memory. A bailout can free that memory and leave the pointers in place, so a later request handled by the same worker reads freed memory. Memory disclosure is a confirmed impact, not just a theoretical one.
Both are rated 9.2 under CVSS v4.0, and both are reachable by an unauthenticated attacker.
The second one is the reason long-lived PHP-FPM workers matter. The bug does not need you to do anything unusual in your application. It needs a worker that survives between requests, which is the entire point of FPM.
| Fixed versions | 8.5.12, 8.4.27, 8.3.36, 8.2.35 — all released 21 Oct 2026 |
| Affected | every release before those, back through PHP 5.4 |
| No fix coming | PHP 8.1 and earlier — end of life, and they are not being patched |
| Exposed instances | ~22.6 million, on Palo Alto’s Cortex Xpanse telemetry |
If you are on PHP 8.1 or below, there is no patch in your future. Upgrading the major version is the remediation, and that is a project, not an afternoon.
WordPress is affected. WordPress is not the bug.
WordPress.org has said no WordPress update is required or planned, which is correct and widely misread. The flaw is below WordPress entirely — it is in the engine that executes WordPress. A site with immaculate plugin hygiene, every update applied, no abandoned code and a clean malware scan is exactly as exposed as a neglected one.
That is an unusual shape for this ecosystem. Patchstack’s 2026 report found 91% of new WordPress vulnerabilities sit in plugins and 9% in themes, with a handful in core, all low risk. Nearly everything we have learned to do about WordPress security is calibrated to that distribution — update plugins, remove abandoned ones, scan for malware artifacts. PHP2Shell sits outside all of it.
The vendors were not asleep — their scanners are just pointed elsewhere
It would be neat to say the WordPress security industry missed this. It did not, and the record is worth being accurate about.
Wordfence issued a PSA to site owners on 21 October, the day of the release. Patchstack published a major security alert the same week. The WordPress-specific coverage was fast, loud and correct.
The gap is not awareness. The gap is instrumentation.
Wordfence, MalCare, Sucuri, Patchstack and WPScan model the WordPress application layer. They match installed plugin, theme and core versions against a vulnerability database, and they hunt for malware artifacts in files and the database. That is the right design for a problem where 91% of the risk lives in plugins — and it means a memory-corruption bug in the Zend engine is not a thing they have a finding type for.
Wordfence does run a PHP version check, and it cannot be disabled. But its published guidance keys on end-of-life major versions — anything before 8.1 is flagged as unsupported. Wordfence’s own documentation even acknowledges that hosts may run older PHP with backported security fixes, which is precisely the ambiguity at the centre of this article. A site on 8.3.5 (unpatched) and a site on 8.3.36 (patched) both read as “PHP 8.3, supported”.
So the honest statement is this: your security plugin’s dashboard will stay green through all of this, and that green is not wrong — it is answering a different question. It is telling you your WordPress is clean. Nobody is telling you your interpreter is.
Treat a green WordPress scan as silence on this issue, not as an all-clear.
Trap one: distro backports make the published advice wrong
This is where most of the damage will be done over the next fortnight, because the instruction being
repeated everywhere — “check that php -v shows 8.3.36 or higher” — is wrong on the two
distributions most Australian SMB sites actually run on.
Debian and Ubuntu do not ship upstream PHP releases. They take the version they shipped at release and backport security fixes into it, bumping only the package revision. The upstream version number never moves.
Ubuntu’s fixed package for 24.04 LTS, per USN-8948-1, is:
8.3.6-0ubuntu0.24.04.13
That package contains the fix for both CVEs. It reports itself as PHP 8.3.6. Debian 12’s fixed
package is 8.2.35-1~deb12u1 and Debian 13’s is 8.4.27-1~deb13u1, which happen to align more
closely, but the principle is identical.
A gate built on the upstream number is wrong in both directions. It fails a patched Ubuntu box, and — because it only ever compares the first three digits — it has nothing useful to say about a box whose package revision is stale.
On Debian and Ubuntu, the only valid check is the package version:
# Ask the package manager, not the interpreter
dpkg-query -W php8.3-fpm
# php8.3-fpm 8.3.6-0ubuntu0.24.04.13
# The decisive check — does the changelog name the CVEs?
zcat /usr/share/doc/php8.3-fpm/changelog.Debian.gz | grep CVE-2026-109618
That one-line grep is the check to run and the output to keep for your records. It names the CVEs and the patch file. A version string does not.
And do not sanity-check a backport by its build date. This catches people who are being more careful than average, which is the irritating part. Security fixes are built under embargo, before the coordinated disclosure date — so the fixed package’s binary can carry a build timestamp earlier than the day the vulnerability became public. On the Ubuntu 24.04 package, the build predates the 21 October disclosure. A reader who reasons “this binary was built before the CVE existed, so it cannot contain the fix” reaches a confident and completely wrong conclusion. The changelog is the only local artefact that settles it.
On RHEL, Alma and Rocky the same logic applies through rpm -q. On cPanel and Plesk, the control
panel’s own PHP manager is authoritative, not the shell.
Trap two: the patch that reports success and changes nothing
The second failure mode is quieter and considerably more dangerous, because every signal you would normally trust comes back green.
Bundled-runtime stacks — the Bitnami-style images, and various one-click marketplace appliances — compile PHP into their own directory tree. The web server in that stack is configured to use that bundled binary. It does not consult the system package manager, and the system package manager does not know it exists.
So on one of those instances:
apt install php8.3runs and reports success.php -vprints a patched version — because that is the CLI SAPI, the system binary you just installed.- Every web request continues to be served by the bundled binary, which has not changed.
- Your monitoring, your version check and your security plugin all report normal.
You have patched a binary that serves no traffic. The vendor guidance in that situation is usually to migrate the instance to a supported image, because the bundled runtime cannot be patched in place — which converts a fifteen-minute update into a host migration under time pressure.
The check that catches it is asking the web SAPI what it is running, not the shell:
# Drop this in the webroot, load it over HTTP, then delete it immediately
<?php echo PHP_VERSION, ' ', PHP_SAPI, ' ', php_ini_loaded_file();
If the version that comes back over HTTP differs from the version php -v prints on the shell, the
shell has been lying to you — and it was lying before this CVE existed.
What to do, in order
- Ask your host in writing, and ask for a package version or an advisory reference. “Security updates are applied automatically” is not an answer to this question. A package string is.
- Patch to 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35, or your distribution’s backported equivalent.
- Restart every PHP worker. This is not optional housekeeping. CVE-2026-109619 is a cross-request bug in long-lived workers — an unrestarted FPM pool keeps running the vulnerable code from memory after the files on disk have been replaced. Rebuild container images rather than patching inside a running container.
- Verify through the web SAPI, not the CLI.
- Check your admin accounts for anything you did not create. There is no official indicators-of-compromise list, so this is ordinary hygiene rather than a specific detection.
If you genuinely cannot patch today
The partial mitigations reduce the attack surface. They do not close it, and the PHP project’s own wording is that they block the confirmed paths but not other variants. Treat them as a way to buy hours, not as a fix:
session.upload_progress.enabled=0- A
post_max_sizeceiling — commonly 8M where the site takes no uploads, 32M where it does - Limits on multipart part counts, field counts, header counts and nesting depth
Do not block multipart/form-data outright. It is circulating as advice, it is not what the
project recommends, and it breaks every file upload and a good many ordinary forms on the site.
CERT-EU’s guidance for organisations that cannot patch a vulnerable site is to consider taking it offline. For a site with no revenue attached, a maintenance page for a day is a legitimate and underrated option.
One more to keep on the list: CVE-2026-115560, which PHP disclosed on 23 October and the 21 October releases do not fix. Patching PHP2Shell does not close it.
Read the trigger conditions before you panic about this one. PHP describes the scenario as
unlikely, because all three must hold: the application sets the session serialization handler to
php_binary or php_serialize, switches it back to php via ini_set() before session_start(),
and leaves session upload progress enabled. Most WordPress sites do none of that. The mitigation
is session.upload_progress.enabled=0, and it has to be set at server or pool level — application
code cannot override a php_admin_* value with ini_set(), which is the whole point of
php_admin_*.
Worth stating plainly because the coverage has blurred it: this is a narrow conditional path, not a second PHP2Shell.
How urgent is this, honestly
The sources disagree, and that disagreement is itself the useful information.
watchTowr reports observing exploitation in the wild through its honeypot network. Patchstack’s telemetry shows probing without confirmed breaches. Tenable says it has not verified that any of the circulating exploit code works, and warns that some of it carries malware. Rapid7’s read is that the public proofs-of-concept each work against one specific combination of operating system, PHP version and web server, and that none is broadly reliable yet.
Taken together: reliable mass exploitation is not here, and the window before it arrives is short. Patchstack’s 2026 report puts the median time from public disclosure to mass exploitation at about five hours. That figure is why “we will do it in next month’s maintenance window” is not a position any more — the patch cycle most SMBs run on is measured in weeks, and the exploitation cycle is measured in hours.
The same report found 46% of vulnerabilities were not fixed in time for disclosure, so a meaningful share of the ecosystem is public and unpatched simultaneously. The assumption that a disclosed bug is a fixed bug has not been true for a while.
The same trap, everywhere else
Once you have seen it here, the shape turns up all over a PHP stack: a command that answers confidently about a runtime it is not part of.
The cleanest example has nothing to do with this CVE. Run php -m on a well-configured production
box and OPcache frequently does not appear — while the FPM pool serving the site has it enabled and
working. That is not a misconfiguration. opcache.enable_cli defaults to 0, because each CLI
invocation is its own process and cannot share cached bytecode with anything. On some builds the CLI
does not load the extension at all, so even phpinfo() from the command line shows no OPcache
section at all, while FPM reads the same settings correctly.
The CLI also reads a different ini file — typically /etc/php/8.3/cli/php.ini, not the FPM pool
config. So a directive you set in the pool is invisible from the shell, and a directive you set for
the CLI never reaches a web request. Some of those settings are INI_SYSTEM scope, which means they
apply at startup and are quietly ignored if you put them somewhere that is read too late. Nothing in
the output tells you any of this happened. You get a value, and the value is true — about the wrong
process.
php --ri opcache # what the CLI actually has
php -i | grep 'Loaded Configuration' # which ini the CLI read
Neither of those tells you what your website is running. Only a request over HTTP does.
What this changes
The instinct after an event like this is to add another scanner. I would not. The three products most Australian SMBs already run would all have stayed green through this, and a fourth one modelled on the same layer would too.
What is missing is not detection. It is an answer to a much more boring question: what is actually executing on this box, and how would we know if it changed? A WordPress site is a stack — the interpreter, the web server, the TLS terminator, the container image, the host — and almost every tool sold to secure it looks at one layer of that stack and reports confidently on the whole thing.
What we need is a patching record that names package versions rather than marketing versions, a check that asks the web server instead of the shell, and a host who will answer the question in writing. None of that is expensive. It is just not what anyone sells.
If you are not sure which PHP binary is serving your site — and after this week, a lot of people are discovering they were not — that is the thing to find out before the weekend.