JAVIER CARUSO
Blog ·

PHP2Shell and the cost of not updating

On October 21, PHP shipped a critical security update. If your stack runs PHP, it should be applied by now. If it doesn't, keep reading anyway.

What happened

On October 20, the PHP project announced a security release for the following day, and public reports were already saying the flaws allowed remote code execution. On October 21, PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 came out, fixing two vulnerabilities: CVE-2026-109618 and CVE-2026-109619, together known as PHP2Shell. Both score 9.2 out of 10 on CVSS 4.0.

The scope is what makes them serious:

One detail that is easy to miss: installing the new version is not enough. You have to restart the processes that serve PHP (the PHP-FPM, Apache, FrankenPHP or LiteSpeed workers) and check that what answers in production really is the fixed version. Until the old process is restarted, the old code keeps running.

And it didn't end there. On October 23, PHP issued an extra advisory for a residual case, CVE-2026-115560, which the project says only affects unusual configurations. As of November 4 it is still unpatched: the fix ships with the next regular release, and in the meantime there is a published mitigation (see the CERT-EU advisory).

You wrote the 5%

When a team thinks about security, it thinks about its own code: form validation, permissions, that one query nobody reviewed. Fair enough. Now do the math on your own project. Underneath what you wrote there is a framework, dozens or hundreds of packages, a runtime, a web server and an operating system. Your code is a small slice of what actually runs in production; call it 5%. You imported the other 95%.

PHP2Shell makes the point uncomfortably clear: teams with spotless code ended up exposed because of a component they didn't write, don't review and, quite often, couldn't tell you the version of.

"If it works, don't touch it"

It is one of the most repeated lines in this industry, and one of the most expensive. It sounds prudent. In practice it is debt, and technical debt compounds.

Going from PHP 8.3 to 8.4 takes an afternoon. Going from 7.4 to 8.4 is a project: years of accumulated language changes, libraries that no longer support your version, and a few that no longer exist at all. You can plan that project on any given Tuesday. On the day a critical vulnerability drops, you don't have weeks. You have hours. And whoever is still on 7.4 that day doesn't have a patch to apply. They have a migration they kept putting off.

An unsupported runtime is a risk you accepted without signing anything

PHP 7.4 stopped getting security fixes on November 28, 2022; PHP 8.0 on November 26, 2023; PHP 8.1 on December 31, 2025. For PHP2Shell the project made an exception and published minimal source-level fixes for those three branches, aimed at people who build PHP themselves. There were no new releases. If your PHP comes from a distribution or a vendor with extended support, the fix may have reached you that way. But it was an exception, not a promise: those branches have no reason to get the next one. Patched or not, you still need to migrate.

How many sites are in that spot? According to W3Techs, as of November 4 PHP runs 69.1% of websites whose server-side language is known. Of those, 26.2% are still on PHP 7 and 7.6% on PHP 5. In other words, more than a third of the PHP web runs on major versions the project no longer maintains. And PHP 8.2, still supported today, stops getting fixes on December 31, 2026.

This isn't a PHP problem. Node.js 18 reached end of life on April 30, 2025, and Node.js 20 on April 30, 2026. Python 3.9 did on October 31, 2025. Nobody signed off on running unpatched software. The date just went by and nobody was looking at the calendar.

Automating the alert is not automating the update

Dependabot and Renovate are good tools. They watch your dependencies, tell you when there is a new version or a known vulnerability, and open a pull request ready for review. The trouble starts when those PRs pile up. With forty of them open, nobody looks, and the day a critical one arrives it gets buried among minor bumps to a date library. At that point the tool is decoration: it gives everyone the feeling that someone is on it, and no one is.

Not every advisory is an emergency (but you have to look to know)

Most security advisories are low severity, hit features you don't use, or sit in development dependencies that never reach production. Treating every one of them as a fire is the fastest way to get a team to stop reading them. But someone has to read them to tell the noise from the signal. PHP2Shell was critical, required no authentication, lived in the runtime and was being exploited. That doesn't get scheduled for next sprint.

Three things to do today

  1. Audit your dependencies. Nearly every ecosystem has a way to do it: npm, Composer, pip, NuGet, Bundler, Cargo. Some build it into the package manager, others ship it as a separate tool. Most take under a minute:
    npm audit                          # npm
    composer audit                     # Composer
    pip-audit                          # pip
    dotnet list package --vulnerable   # NuGet
    bundle audit                       # Bundler
    cargo audit                        # Cargo

    pip-audit, bundle audit (from the bundler-audit gem) and cargo audit are separate installs. The total count matters less than what the critical and high findings say. One caveat: an audit checks your packages, not your runtime. A clean report says nothing about whether your PHP, Node or Python version is still supported; that is step 2.

    For a second pair of eyes, give an AI like Claude or ChatGPT the list of your dependencies and their versions (what your package.json, composer.json, requirements.txt, Gemfile or .csproj says) and ask it to flag what is outdated or out of support. Just the list: no credentials, no private URLs, nothing internal. It helps you sort things out, but verify afterwards with the audit and the official sources, because the model may not know about last week's release.

  2. Check your runtime version and its end-of-life date. PHP, Node, Python, .NET, Java, whatever you run. endoflife.date has nearly all of them in one place. If the date has passed, you have a project on your hands. If it passes in the next six months, you have one too.
  3. Count your open Dependabot or Renovate PRs. If there are more than three, block two hours this week to go through them. You don't need to merge them all; you need to know what's in there.

Keeping dependencies current is not glamorous, and it will never be the feature that makes the demo. There will be another critical vulnerability in something you imported; that part is not up for debate. What you do get to decide is how far behind it finds you. And if someone asks "how does this pay for itself?", the answer is easy: it costs a lot less than the alternative.

Sources

This piece deliberately leaves out any technical detail on how the flaws are exploited. If you need to mitigate, go to the official PHP advisory or the CERT-EU one.