Technical debt is a vulnerability with a nicer name
The PHP2Shell postmortems showed the same truth twice: the bug class was known for years and shelved as too difficult to exploit, until AI made exploitation cheap. Every end-of-life runtime, abandoned plugin and deferred upgrade on your stack is pre-positioned for exactly that moment. How to inventory the debt, order the burn-down by exposure, and fund it as maintenance rather than heroics.

By Sam Moreton
Researched
Every security team has a list they keep meaning to get to. The PHP2Shell postmortems, covered in our full timeline, put a date on what happens to that list: a PHP core developer found the out-of-memory corruption during an audit in 2024, the class of bug had been flagged as a concern decades earlier, and the team shelved it as too difficult to weaponise. That judgment was correct for humans. It did not survive twenty-four months of AI-assisted vulnerability research.
This is part two of our series on the November that made continuity planning urgent. Part one covered the supplier you cannot patch. This one covers the debt you can.
Debt is pre-positioned attack surface
The uncomfortable reframe: technical debt is not a cleanliness problem that occasionally has security consequences. It is attack surface with a payment schedule. An end-of-life runtime is a standing promise that no patch will ever arrive for whatever is found in it next. An abandoned plugin is code whose vulnerabilities become public while your copy stays frozen. A deferred framework upgrade is a widening gap between the bugs being found upstream and the version you run.
The PHP2Shell long tail is the demonstration. Servers on PHP versions without official patches, appliances nobody owns, forgotten virtual machines: an entire economy of pre-positioned debt, harvested quietly in the weeks after disclosure. Nothing about that harvest required sophistication by historical standards. It required only that the debt existed and that someone finally came to collect.
The inventory that matters
One page, six layers, an afternoon:
| Layer | What to record | The question |
|---|---|---|
| Runtime | PHP, Python, Node versions | Is this version still receiving security fixes? |
| Web server | Apache, nginx builds | Whose package, and do backports reach it? |
| Framework | Laravel, Next, WordPress core | How far behind current stable? |
| Extensions | Plugins, modules, dependencies | Abandoned by its author, yes or no? |
| Images | Container base tags | When was the base last rebuilt? |
| Plumbing | CI runners, build tools, agents | Who patches these, and do you know they exist? |
The last two rows are where inventories usually fail. Build tooling is internet-exposed, rarely audited, and inherits every property of debt while being owned by nobody. If the PHP2Shell timeline taught one diagnostic lesson, it is the Apache backport trap from part one's fortnight: distributors backport fixes into old version numbers, so the number your server reports answers nothing. What answers the question is the vendor's advisory for the installed package. Record the package source alongside the version, or the ledger lies to you.
Order the burn-down by exposure, not annoyance
Debt retirement fails when it is sequenced by how irritating the work is. Sequence it by how reachable the surface is:
- Internet-facing and unauthenticated. Public endpoints, the web server, anything with an exposed admin login. This is where automated scanning lives, and it is where PHP2Shell's harvest happened.
- Internet-facing but authenticated. Customer portals, dashboards. One phished credential away from tier one.
- Internal but privileged. CI, build agents, backup infrastructure. The supply-chain tier, which part four of this series takes further.
- Internal and low-privilege. The intranet wiki from 2019. Real debt, low collection risk, and it can wait its turn.
Each row in the ledger gets a date and an owner. The date matters more than the speed: PHP2Shell's lesson is not that everything must be current, it is that everything must have a decided future. An EOL version with a scheduled upgrade is a plan. An EOL version with no opinion attached is the vulnerability.
Funding it without heroics
The pattern that works treats upgrades as rent, not renovation. A recurring line in the maintenance budget, consumed monthly, rather than a quarter-long project requested in a panic after the bad week. The monthly maintenance routine we recommend for WordPress sites is the same discipline at application level, and it exists because the panic version of this work costs multiples and ships worse.
For agencies and anyone running a fleet, the multiplier runs backwards: the ledger is per stack template, not per server, and retiring debt once retires it everywhere the template is deployed. That is the same leverage managed platforms apply at their layer, and it is the strongest argument for keeping the fleet uniform rather than bespoke.
Where the platform carries this for you
To be precise about the split, because it matters: on a managed platform the runtime and web server rows of that table are the operator's debt. When October's PHP flaws landed, the fleet was patched including end-of-life versions back to 5.6, and when the November nginx advisories arrived the upgrade rolled out with no customer action, both on the public status record we documented in the PHP2Shell piece. On an unmanaged server those rows are yours, on a Friday, in a queue.
That is the honest case for running on a panel like Cloudways rather than a raw VPS: not that debt disappears, but that the deepest and slowest-moving debt, the layer you cannot patch from inside your application, belongs to a team whose entire job is that ledger. Your rows, application code and extensions, stay on your page either way.
The ledger is the deliverable
The instinct under deadline is to fix things and feel productive. The asset this piece is arguing for is the page: every layer, version, package source, EOL date, owner and decided future. The fixing follows from the page, in exposure order, at maintenance speed.
The next PHP2Shell is not a question of whether but of when, and the teams that sleep through its disclosure week will be the ones whose ledgers already said so, with dates attached. The teams that do not will be re-deriving this entire list from memory, during the incident.
Part three covers the tooling that keeps the ledger honest: the same class of AI that found these flaws, pointed at your own stack on a schedule.
FAQ
Where do I start when everything feels out of date?
With the internet-facing layer: the web server, the runtime behind it and whatever faces the public. One page listing each layer, its version and its end-of-life date. The order of work is exposure, not annoyance, and the first page usually takes an hour.
How is this different from normal maintenance planning?
The frame. Maintenance planning asks what breaks if we do nothing and weights by inconvenience. Security debt weighting asks what an attacker reaches first, and the PHP2Shell lesson is that the answer changes faster than release calendars do. Same ledger, different sort order.
We are on managed hosting. Is this still our problem?
Less of it, not none. The platform patches the runtime and web server layer, which is the deepest and slowest-moving debt. Your application code, plugins and integrations remain yours, and the EOL ledger still needs an owner.