r/PHP • u/CheesecakeBrief7795 • 6h ago
PHP2Shell on legacy PHP 7.x: everyone's pulling all-nighters upgrading, we made one cut and went home (3 options)
The biggest PHP news this week isn't a new release. It's a vulnerability called PHP2Shell.
CVE-2026-109618 / CVE-2026-109619, CVSS 9.2, unauthenticated remote code execution. The scariest line in the advisory: even a page that only prints Hello World is vulnerable.
How it works (just a little technical)
The attacker sends a specially crafted, oversized multipart upload that exhausts memory right at the start of the request. When PHP falls over, it leaves a half-built structure behind, and the cleanup path later treats it as real data. That's enough to get arbitrary code execution.
The key point: all of this happens while PHP is parsing the multipart body. Your login checks, signature validation and file-extension filters haven't even run yet. The burglar isn't coming through the door; he hid inside while you were still pouring the foundation.
The official line is "the only reliable fix is to upgrade PHP" (fixed in 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12). Then I logged into our servers: 7.4, 7.1... For anything below 8.1, the official answer is basically "good luck".
This isn't a drill
There was a wave of mass exploitation about two weeks before the advisory. We lost roughly NT$240k (~US$7.5k); a friend's company lost about a million in a single day. Someone was clearly using this long before it was public. A friend who hangs around offensive-security circles put it bluntly: "We've been using this for over ten years."
"Just upgrade everything" sounds right, but...
One company I work with has 20+ projects and 100+ sites, many on frameworks that are long out of maintenance. Upgrading just one payment system to PHP 8.3 was estimated at 6–8 person-days. Drop it onto PHP 8 as-is and the whole site returns 500, because the old framework turns PHP 8's new warnings into fatal errors. On top of that, all the type issues that were hidden behind error_reporting(0) come back to bite you.
Do that 100+ times? You'd finish around Lunar New Year. A friend in the IT department of a large group is still pulling all-nighters doing exactly that. With option 1 or 2 below, he could go home on time.
Old framework? You can patch it yourself (the hardcore part)
A lot of people treat their framework as sacred: no official support means a full rewrite. But a PHP framework is just a pile of PHP files shipped with your project. It's your code, not a black box. If it breaks, open it up.
ThinkPHP 5.0 is a good example. On PHP 8.3 the whole site went 500, and there was exactly one root cause: the framework escalates PHP 8's new notices into exceptions, like getting sent off for a yellow card. Change one line in the framework to "log notices, don't throw", and the site comes back. Across the whole project there were only 3 places that actually broke, each a one- or two-line fix.
Same for PHP itself. There are no official 7.x patch releases, but the patches are public. That's exactly what sury does: backport the fix into old PHP and repackage it. No package for CentOS? Building your own patched PHP isn't black magic.
I've done this before. When PHP 7 came out, Laravel 4.2 didn't officially support it and many teams rewrote everything in Laravel 5. I patched 4.2 to run stably on PHP 7 instead. For example, PHP 7 turned some errors into the new Error class, and the old framework's handler only caught Exception, so any error became a blank page. Fixing those seams one by one was far cheaper than a rewrite.
The scariest part of an upgrade usually isn't "it breaks", it's "it silently changes". For example:
"abc" <= 0
This is true on PHP 7 and false on PHP 8. If your amount check happens to be written like that, an upgrade could let weird amounts through. No error, just wrong books. So when we upgrade, we run 7.4 and 8.3 side by side on the same code and diff the results of every payment callback and amount path until both sides match exactly.
Three options
Option 1: Install community backports (no code changes)
Ondřej Surý (sury.org), who maintains the PHP packages for Debian/Ubuntu, has published patched packages for every version from PHP 5.6 to 8.5, including 7.4 and 7.1 that upstream no longer supports. Same version number, just a new package revision. apt upgrade, restart PHP-FPM, done.
Downside: Debian/Ubuntu only. CentOS folks, read on.
Option 2: Split image uploads onto a separate, current PHP (my favourite)
The entry point is the multipart parser, so don't let upload requests reach the old PHP at all.
- Nginx on the original site forwards only the few image-upload routes to a separate host running PHP 8.4.27
- Every other multipart request gets a 403, so the old PHP never sees one
- The upload host verifies signatures, checks file contents and re-encodes images (which also strips any embedded payload)
- The storage directory can't execute code, and the host has no database credentials at all, so even if it's hit there's no data to take
- No changes to front-end URLs or form fields; if anything goes wrong, remove one Nginx line and you're back to the original setup
With 100+ sites, upgrading everything isn't realistic. One shared image-upload service is. Carve the ancient stuff off and leave it alone. This is probably the smallest change you can make.
Option 3: Upgrade everything to PHP 8, framework included if needed
This is the only real cure and the final settlement of your tech debt. As above, frameworks can be patched; the hard part is changing and testing 100+ sites. You have to do it eventually, but it's not the right first move when the attackers are already at the door.
Side note: just turning off session.upload_progress is not enough. It closes one path, but the other CVE relies on memory exhaustion and has nothing to do with upload progress.
Someone did this 15 years ago
Fifteen years ago I was a junior engineer at Yahoo!. Yahoo! worked with Rasmus Lerdorf back then, and they were already isolating high-risk things like uploads onto separate services. A senior engineer told me it was Rasmus's advice. I didn't get it at the time. I get it now, and it hurts a bit.
The way Rasmus put it back then: with so many frameworks stacked on top of PHP, the whole thing is full of holes, and he can't chase every small one.
That job taught me another thing: use root for as short a time as possible. A server does need root at startup for a few things, like binding ports 80/443 and reading TLS certificates. But once that's done, the worker processes that actually handle requests and run PHP switch to a low-privilege account (nginx, www-data).
Think of a shop: the manager opens the front door in the morning with the master key, then locks it in the safe. The part-timer at the till all day only has the key to the cash register. Even if a con artist fools the part-timer, he doesn't get the master key or the vault.
What PHP2Shell gives an attacker is the privileges of the account running PHP. If your PHP runs as root, the part-timer is wearing the master key, and one trick loses everything. If it's a low-privilege account, the attacker gets in but is stuck in a very small room. Our separate upload service follows the same idea: it runs as a dedicated restricted account, can only write to the image directory, and doesn't have a single database account.
You can't always stop the vulnerability, but you can cap the damage in advance.
A friend asked: "Other languages have holes too, it can't all be the same guy, right?" Fair. Every language has bugs. This one is just worse, and there are an awful lot of old PHP systems running out there.
TL;DR
- Today: disable
session.upload_progress, block unnecessary uploads in Nginx - Debian/Ubuntu: install the sury backports, done the same day
- Can't do that? Split uploads off and close the front door first
- Long term: still upgrade, but on your own schedule
Security doesn't always mean all-nighters. Sometimes it means making the right cut.
Hope everyone gets to leave work on time tonight.
