polycratia

No Patch Is Coming: PHP 7.4, 8.0 and 8.1 After PHP2Shell

· 9 min read

On 21 October 2026 CISA issued an alert and CERT-EU published advisory 2026-018, covering two remote code execution flaws in PHP core: CVE-2026-109618 and CVE-2026-109619, both rated CVSS 9.2. An unauthenticated attacker can run code on a web-facing PHP runtime. Emergency releases exist for PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35. For PHP 7.4, 8.0 and 8.1 there is no fix, and there will not be one. That second sentence is the whole story, and it is not a security story.

What happened#

Both flaws sit in core, not in an extension and not in a framework. Per the php.net release notes and the two advisories, they reach every mainstream way of serving PHP over HTTP: mod_php, PHP-FPM, php-cgi, FrankenPHP, LiteSpeed. No configuration turns a web-facing runtime into a non-web-facing one, which is why CISA and CERT-EU wrote the same sentence: patch immediately. Exploit attempts have been reported.

Two days later, on 23 October, a third flaw was disclosed as CVE-2026-115560. It has no patch yet. The only published mitigation is one line of configuration:

ini
; php.ini
session.upload_progress.enabled = 0

One piece of noise worth naming, because it wasted people's morning: details leaked on Hacker News before the official release, carrying an inflated CVSS 10.0. The official rating is 9.2. Operationally that difference is nothing. Unauthenticated remote code execution at 9.2 and unauthenticated remote code execution at 10.0 produce the same day of work. Argue about the decimal later, since the decimal has not once changed what you have to do.

What to do today if you stay on PHP#

Four things, in this order.

Move to a patched build. The only versions carrying the fix are 8.5.12, 8.4.27, 8.3.36 and 8.2.35. If you are on 8.2 or newer, this is a package upgrade and a restart, and you should be done before lunch.

Set session.upload_progress.enabled = 0 for the third CVE, then verify the setting actually landed in the SAPI that serves traffic. The CLI ini file is frequently not the FPM ini file, and the mitigation in the wrong file is worth exactly zero.

Verify what your hosting vendor patched. Not the status page, the running binary. A status page saying 'PHP updated' is a marketing artifact: the version of the process answering port 443 is evidence. Ask for it explicitly, and ask which SAPI.

Then inventory every box, including the ones nobody calls a web server:

bash
# each SAPI reports its own build; the web one is the one that gets exploited
php -v
php-fpm -v
php-cgi -v 2>/dev/null

# the running process, not the package manager's opinion
pid=$(systemctl show -p MainPID --value php8.2-fpm)
readlink -f /proc/$pid/exe

# confirm the mitigation in the ini set that FPM actually loads
php-fpm -i 2>/dev/null | grep -i upload_progress
grep -RIn 'upload_progress' /etc/php/

# the hosts people forget: cron, queue workers, build images
crontab -l | grep -i php
grep -RIn 'php' /etc/cron.d/
docker ps --format '{{.Image}}' | sort -u

A cron box running an unpatched build is still an unpatched build. It is less exposed, not safe, and it is usually the one holding credentials for everything else.

The part that matters more than the score#

If you are on 7.4, 8.0 or 8.1, none of the above finishes the job. You can apply the one-line mitigation. You cannot apply the fix, because the fix was never built for your branch.

Here is what I want to be precise about: an unpatchable branch is not a security incident. It is a stack decision, made years ago, by not making it. Nobody approved staying on 7.4. The branch went out of support while the service kept working, and working software generates no tickets. PHP2Shell did not create that position. It published it.

And it will happen again. The structural fact about an end-of-life runtime is not that it has one unfixed hole today; it is that every future CVE lands on it with no upstream answer. You are no longer in a patch cycle. You are in a sequence of one-off mitigations, each one narrower than the last, each one depending on whether a workaround happens to exist for that particular bug. This one has a config flag. The next one may not.

So the honest question today is not 'how do I patch this'. It is: version jump, or stack jump?

Version jump or stack jump#

Four signals, and you can read all four in an afternoon.

Size. Not line count, surface. How many HTTP entry points, scheduled jobs and external integrations does the service own? A service with twelve endpoints and one payment provider is a different animal from one with two hundred endpoints accumulated over eight years.

Glue versus rules. This is the decisive one. How much of the code is framework scaffolding, and how much is business logic that only exists here? Glue is cheap to replace: it is a solved shape, and you are rewriting something whose behaviour is documented. Business rules get expensive, because the rules are not written down anywhere except in the code, and half of them are bug-compatible with a client's expectations.

bash
# rough shape: where do the lines actually live
find . -name '*.php' -not -path './vendor/*' -print0 | xargs -0 wc -l | tail -1
find . -path './vendor/*' -name '*.php' -print0 | xargs -0 wc -l | tail -1

# controllers and views are glue; count them separately from the rest
find . -path ./vendor -prune -o -name '*Controller*.php' -print | wc -l
find . -path ./vendor -prune -o -name '*.tpl' -o -name '*.phtml' -print | wc -l

# every place money, auth or an external party is touched
grep -RIl --include='*.php' -e 'curl_init' -e 'password_' -e 'session_start' . | grep -v vendor | wc -l

Test coverage. Not the percentage. Whether any executable description of correct behaviour exists at all. With a test suite, a migration is mechanical and verifiable. Without one, the only available oracle is the old service itself, which changes the plan (see below).

Who still knows it. If the people who wrote the business rules are gone and the rules are undocumented, a version jump on an unsupported branch buys you very little: you are paying migration cost to land on another runtime that will also age out, with nobody able to confirm you preserved behaviour.

Two strong branches and a staffed team: jump the version, stay on PHP, get back into a patch cycle. Mostly glue, thin rules, no owner left: moving off the branch entirely is probably cheaper than nursing it, and it ends the recurring exposure instead of deferring it.

What an honest rewrite looks like#

Scope first. A day of reading a service tells you what it costs and how many weeks it takes. That is a real deliverable, and it is the only part that can be done quickly. Anyone quoting a rewrite without reading the code is quoting a different service than the one you have.

Strangler, not big bang. The old service keeps serving traffic. New endpoints come up behind the same entry point and take routes one group at a time, and each cutover is individually reversible. A big-bang rewrite has exactly one integration test, on the worst possible day.

Data model, then payment and auth paths. The schema is the hard constraint everything else gets shaped around, and the money and identity paths are where a silent behaviour difference costs real money rather than a bug report. Do them while attention is highest, not last.

Keep the old service running as the reference until parity. With no test suite, the legacy code is your specification: you run both, compare outputs on real traffic shapes, and the differences you find are the undocumented rules. That is how you discover the rounding rule nobody remembers writing.

Timelines, honestly: a small service with clear boundaries moves in days. A large one moves in planned weeks, endpoint group by endpoint group, with traffic shifted gradually. Nothing meaningful gets rewritten in a day. What takes a day is knowing the number.

What I would do differently#

I would make the support window a build-time fact rather than a thing someone remembers. A pipeline check that fails when the runtime branch has fewer than six months of upstream support left turns this from an emergency into a scheduled quarter of work. The cost of moving a service does not grow because the code got worse. It grows because the distance to a supported branch gets longer every year you stand still, and because the people who understood the rules keep leaving.

The branches that got patched this week will be unsupported too, on a published schedule. The question that matters is whether you meet that date deliberately.

Getting a number#

If you are sitting on 7.4, 8.0 or 8.1 right now, the useful next step is not a debate about risk appetite. It is a cost and a timeline. Send me the service and within a day you will have both: what moving it onto a supported, modern stack costs, and how many weeks it takes. I have done inherited-system rewrites and major runtime migrations before, including monolith-to-services splits where the old system stayed in production as the reference the whole way.

Apply the mitigation today either way.

react

$ new-project --brief

or email hey@polycratia.com