PHP2Shell, and a Site Stuck on PHP 5.6
Two critical PHP bugs, one ExpressionEngine 2 site with no upgrade path.
On October 21 the PHP project published fixes for two bugs, CVE-2026-109618 and CVE-2026-109619, rated 9.2 out of 10. Together they let anyone on the internet run code on a PHP web server by sending it a crafted request. The details had leaked a day earlier, and exploit attempts showed up in honeypots before the patch was out. Apache with mod_php is the worst case: the attack runs before the application gets to execute a single line. Fixed releases exist for PHP 8.2 and up. Everything older gets nothing.
I run a site for the Malaysian-German Society. It’s ExpressionEngine 2.9.2, a CMS release from 2014, on PHP 5.6, on Apache with mod_php. The day before I’d applied the two workarounds from the advisory: no memory limit inside PHP, upload progress off. The advisory itself says those reduce the exposure and don’t close it. CISA’s advice for anything on PHP 8.1 or older that can’t be patched is to take it offline.
On my day off, I decided to take another look:
The options
ExpressionEngine 2 was a commercial product. The last 2.x release got PHP 7.0 support in 2016; 2.9.2 predates even that. So the choices were:
- Upgrade to ExpressionEngine 7. It’s open source now and runs on PHP 8.5. The catch is the 17 add-ons: a couple are discontinued, two keep their content in their own tables, and every template that touches them changes.
- Replace the CMS with a static site. Someone still edits events and news in the control panel, so no.
- Patch the CMS myself for PHP 8.4. A fork of a 2014 code base, maintained by me.
I’d have told you the third one was the worst idea until I looked into it… A PHPCompatibility scan over the whole tree came back with 157 errors in 31 files: 60 PHP 4 style constructors, 41 each() loops, 140 curly-brace string offsets that phpcbf fixes on its own, and the removed mysql extension, which CodeIgniter 2 already has a mysqli driver for. Nothing too crazy.
How it went
The mechanical part was one script. The rest was iteration:
- boot the site on
php:8.4-apache - load a page
- read the error
- fix it
- reload
ExpressionEngine turns a fatal error into a blank page with a 200 status and nothing in the log, so I added a shutdown hook that logs the last error.
The runtime breakage was about twenty items, and a few were real bugs that PHP 5 had hidden for a decade. A + where . was meant, which PHP 5 evaluated as zero and PHP 8 refuses. A regex [^\w-.] that the newer regex engine rejects, so a column name came back empty and the comments page died on a SQL error. count() on a string, which used to return 1. And 74 methods in add-ons whose signatures don’t match the class they extend, which PHP 8 won’t load at all; a small script found all of them in one pass.
Result
130 files, about 390 lines changed. The site has been on PHP 8.4.27 since the afternoon of the 23rd. I compared 29 pages before and after the switch: identical status codes, sizes within a few hundred bytes.
The ExpressionEngine 7 question is still open. It’s just no longer urgent.