On 21 October, PHP published security releases for a set of interpreter bugs that researchers at Wiz named PHP2Shell. The bugs are tracked as CVE-2026-109618 and CVE-2026-109619. Chained together, they gave the researchers unauthenticated remote code execution against current WordPress, Drupal and Joomla container images.
The important detail is where the bugs live. They are not in WordPress, Drupal or any plugin. They are in PHP itself. A "Hello World" script on an affected PHP version is exposed. This post covers what happened, why no code scanner (ours included) could have flagged it, and the steps that do help this week.
What the bugs are
Every PHP request runs inside a worker process that handles many requests in turn. The bugs are in how PHP cleans up when a request dies from a fatal error, especially running out of memory.
- An attacker sends a multipart upload request built to push the worker past its memory limit while PHP is still setting up the request.
- The fatal error interrupts cleanup. Some internal pointers are left pointing at memory that has already been freed. They include the session id, the unserializer's state, the saved backtrace of the fatal error, and part of OPcache's shared bookkeeping.
- The next request handled by the same worker reuses that stale state.
From there the research describes two kinds of damage. The first is a memory leak to the attacker: a stale pointer is written back out inside a PHPSESSID cookie, carrying whatever the worker had in that memory. That can include passwords, tokens and keys, and it tells the attacker where things sit in memory. The second is memory corruption through the stale unserializer, backtrace and OPcache state. Combined, the leak removes the guesswork and the corruption turns into code execution.
Three properties make this serious:
- No authentication. The leak works before any application code runs. It works even if the application never calls
session_start(). - Every framework. Because the bugs are in the interpreter, the application on top does not matter.
- A decade of versions. According to the research, every PHP release from the last ten years carries at least one of the bugs, back as far as PHP 5.4.
The researchers also make a point worth repeating. The flawed code had been in PHP for more than a decade. What changed is that frontier AI models made it practical to chain the bugs and adapt the exploit to each target's memory layout, work that used to take a specialist team months. Bugs that were once too awkward to exploit are becoming worth the effort, and patch windows shrink accordingly.
Why a code scanner could not catch this
We build a static security scanner, so we want to be direct about this. Vulkro would not have found PHP2Shell, and neither would any tool that reads application code.
- Code analysis looks for unsafe flows in your code. Here there is nothing unsafe in your code. The flaw is in the program that runs it.
- Dependency analysis reads
composer.lockand matches your libraries against advisories. PHP is not incomposer.lock. The"php": ">=8.1"line incomposer.jsonis a constraint, not the version you run. - Container image scanning matches the packages installed in an image against distribution advisories. That works when PHP comes from the distribution's package manager (for example Debian's
php8.2packages), once the distribution publishes its advisory. It does not work for the officialphpDocker images, the base of most WordPress, Drupal and Joomla images. Those images compile PHP from source into/usr/local, so the package database never lists PHP at all.
Before the disclosure, nothing could find these bugs short of the research itself. After the disclosure, what matters is knowing where you run PHP and which version it is. The rest of this post is about getting that answer quickly.
What to do this week
1. Find every place you run PHP
Start with source repositories, then the images you actually deploy.
grep -rniE '^\s*FROM\s+\S*php' --include='Dockerfile*' --include='*.dockerfile' .Official PHP images record their version in the image configuration. That is the quickest reliable check on a built image:
docker image inspect --format '{{range .Config.Env}}{{println .}}{{end}}' your-image:tag | grep PHP_VERSIONFor an image that installs PHP another way, or a server, ask the binary itself:
docker run --rm --entrypoint php your-image:tag -vDo not trust tags alone. php:8.3-apache built last month carries last month's PHP. The version inside the image is what counts, and an image only picks up the fix when it is rebuilt from an updated base.
2. Patch, rebuild and redeploy
The researchers describe upgrading PHP as the only complete fix. Take the fixed version for your release line from php.net, rebuild every image from the updated base, and redeploy. Pinning a base image by digest is good practice, but it also means nothing changes until you update the pin.
If PHP comes from your distribution's packages, the fixed version may keep the old upstream number with a distribution suffix, because distributions often backport fixes. Check your distribution's security tracker rather than comparing upstream version numbers.
3. Reduce exposure while you patch
If some systems cannot be patched today, put protection in front of them. Several web application firewall and hosting providers have published rules for this attack. Treat them as a way to buy time, not as a fix.
4. Look for signs of attempts
Because the attack happens inside the interpreter, application logs may show little. The researchers suggest watching for:
- unusual multipart requests followed by memory-limit errors
- bursts of HTTP 500 responses
PHPSESSIDcookies in responses that are unusually large or malformed- PHP workers restarting more often than usual
A PHPSESSID cookie in a response from an application that does not use sessions deserves a closer look.
The longer lesson: track the runtime like a dependency
Most teams have a good answer to "which version of this library do we run, everywhere?" and a weak answer to the same question about the language runtime. The interpreter is the largest piece of third-party code in a PHP, Node.js, Python or Java application, yet it often sits outside the dependency inventory entirely: set once in a Dockerfile, refreshed whenever someone remembers to rebuild.
Three habits close most of that gap:
- Record the runtime version where you record dependencies. Your SBOM should list the interpreter and its version, not only the packages your manifest names.
- Rebuild on a schedule, not only on code changes. An image that is not rebuilt never receives a runtime fix, however current its base tag looks.
- Know which runtimes are past end of life. An unsupported PHP or Node.js version gets no fix for bugs like these, so it can never be patched, only replaced.
PHP2Shell will not be the last interpreter-level bug, and it will not be limited to PHP. The teams that patch it fastest this week will be the ones that can answer "where do we run this, and which version?" in minutes.
Sources
- PHP2Shell: the original research by Wiz, which describes the bugs, the exploit chain and the indicators above.
- PHP releases on php.net, for the fixed version on each release line.