the writing notes 12 min

PHP2Shell: the PHP vulnerability that argues for going headless

Governments pulled sites offline on 21 October because PHP sat in every request path. Static and headless builds stayed up. Here is the decision table.

Cracked server tower with a broken padlock and a seashell on top, linked by one cable to a separate browser window.

On 21 October the Dutch national cyber security centre took its own website offline. Not because WordPress had been hacked, but because the PHP interpreter underneath it could be.

That day the PHP project shipped fixes for two unauthenticated remote code execution bugs in the interpreter itself, CVE-2026-109618 and CVE-2026-109619. Any site where PHP answered public requests had to patch or go dark. Sites that served static HTML from a CDN had no PHP process in the request path, so there was nothing to patch at the edge. Headless WordPress still needed its backend updated, but the public site stayed up. Below: the timeline, the architectural argument, a decision table by stack, a 30/60/90-day plan, and a checklist if you are staying on PHP for now.

If you run PHP, do this today.

  • Confirm the running version is 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or a vendor backport whose notice you have read, and restart every worker.
  • Check what your hosting control panel actually shipped, not what the dashboard says.
  • Treat PHP's ini mitigations and any WAF rule as a bridge to the patch, not a substitute for it.
  • On 7.4 to 8.1, ask your host whether it built the security team's source backports, and book the upgrade either way.

What happened on 21 October 2026

The timeline matters because it explains the panic. According to Tenable, the CVE identifiers circulated on Hacker News and social media on 20 October. Later that day the PHP security team published an advance notice of a release for 21 October at 14:00 UTC, saying it could ship earlier if details became public. Tenable also reports, citing watchTowr, that disclosure had originally been set for 22 October under embargo.

The PHP security team then published its advisory on 21 October, titled "Security advisory: Critical remote code execution vulnerabilities, update now" (php.net news archive). The ENISA joint statement puts the updates at 13:40 UTC. Fixed versions are PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35.

The two bugs, from the GitHub advisories:

  • CVE-2026-109618: remote code execution via out-of-memory corruption.
  • CVE-2026-109619: remote code execution via stale global state, a use-after-free between requests handled by the same worker.

The PHP security team rates both CVSS v4.0 9.2. Tenable notes that some early reports, including watchTowr and a Hacker News post, said 10.0. Every web-serving SAPI is in scope: PHP-FPM, Apache mod_php, php-cgi, FrankenPHP and LiteSpeed. The OOM advisory singles out mod_php as the most exposed, because the failure happens entirely before any application code runs. Calif says the earliest affected code path goes back to PHP 5.4.0. The discovery is credited to Wiz, Calif and OpenAI, with AI-assisted research.

Then the outages. Reuters and RTE reported government services taken offline in the Netherlands, Finland, France and Australia, including the Dutch national cyber security centre's own site. CERT-EU recommended isolating internet-facing applications until patched, and CISA urged immediate patching. Krebs reports Cloudflare deployed WAF rules at 3:02 am UTC. Amazon Threat Intelligence says it saw an attempt on 17 October and later described exploitation as broad.

On exploitation, the sources disagree, and you should know that. The PHP Foundation said it had not identified functional exploits at the time of release. Krebs reports experts saying working exploits exist. Tenable said code claiming to exploit the bugs was circulating but it had not verified any of it.

Why the advice was patch now or go offline

Unauthenticated remote code execution in the request path means the web server is the target. There is no login to put in front of it, no plugin to disable. A request arrives, PHP parses it, and the bug is already in play. The OOM advisory confirms impact as unauthenticated code execution as the web worker account, validated over HTTP against mod_php with default input and memory limits.

The mitigations PHP listed are real, and they are stopgaps. The advisories say:

  • Set memory_limit=-1 administratively (via php_admin_value), keep post_max_size conservative, and block multipart requests at the frontend if you do not use them.
  • Set session.upload_progress.enabled=0.
  • On PHP 8.5, set fatal_error_backtraces=0.
  • Restart every worker after any of these.

The same advisories are blunt that updating is the only complete fix. For the stale-state bug there is no complete generic configuration mitigation at all. PHP core developer Gina Peter Banyard, quoted by Tenable, said the only mitigation is patching or upgrading PHP, and PHP 8.6 release manager Joe Ferguson said ini settings won't save you. So treat the list as a bridge across an afternoon, not a posture.

Two practical snags made the day harder than a version bump. Shared hosting and control panels ship PHP packages on their own schedule, so the version you think you run may not be the version running. Plesk published its own notice for the PHP it ships, which is the kind of page you need to read rather than assume.

And then the older branches. The php.net advisory says PHP 8.1 and older no longer receive updates from the project and points to distributions and LTS vendors for backports. The security team did publish minimal source-backport branches for 7.4, 8.0 and 8.1 on GitHub, but a source patch is not a release: someone still has to build it and ship it to your host. PHP 5.4 through 7.3 get nothing. If you never upgraded, that was your 21 October.

Some numbers on scale. W3Techs shows PHP on 69.3% of websites with a known server-side language as of 22 October, with 65.3% of those on version 8, 26.9% on version 7 and 7.7% on version 5. Censys counts 2.58 million internet-facing hosts and 54.9 million web properties running PHP. That is a lot of workers to restart.

Who had nothing to do on 21 October

Here is the exposure in request-path terms. Ask one question of any site: when a visitor loads a page, what code runs to answer them?

Elephant on a server box beside a broken padlock, with a detached browser window linked by a single thin cable.

Static HTML on a CDN

If the answer is "nothing, the CDN returns a file", PHP2Shell does not touch the public site. No PHP process answers requests, so there is nothing to patch at the edge and no reason to take anything offline. I run gautamkhorana.com on Astro with static output. HostList, which I built on Next.js and Supabase with about 28,000 programmatic pages, had no PHP process to patch. Neither did Deluxe Astrology, which runs 91,000+ pages in 30 languages. Nobody got a call on 21 October.

Headless WordPress

Headless is a different story, and I would rather be precise than flattering. The PHP backend still needed 8.5.12, 8.4.27, 8.3.36 or 8.2.35 that day, because WordPress still runs on PHP. The difference is where it sits. Behind a firewall or an IP allowlist, with the public site served by Astro or Next.js, the backend was not in the path of a visitor's request. The patch was a maintenance task, not a public outage.

Say it plainly: this is not "headless is unhackable". An attacker who can reach the backend can still reach the bug, which is why the allowlist matters. SSR frameworks carry server-runtime risks of their own, covered in the Next.js, Astro and Nuxt security piece, and the broader comparison lives in headless vs WordPress security. I will not repeat either here.

A pattern, not a one-off

This was not the first time PHP's request path made headlines. PHP-CGI argument injection, CVE-2024-4577, landed in 2024. There was an older PHP-CGI bug in 2012. Now PHP2Shell. Different flaws, same shape: code that sits in front of every public request gets hit, and everyone downstream scrambles.

Now the lesson that cuts the other way. React2Shell on Next.js showed that SSR runtimes carry their own request-path exposure. If a JavaScript server answers every page request, it is the new PHP for patching purposes. Moving from WordPress to a Next.js server-rendered site moves the attack surface. It does not delete it. Static output is the strong claim; SSR is a trade.

So the question was never "which language". It is "what code answers a public request". Cut that list down and your incident days get shorter whatever the next CVE is written in.

There is a forward-looking point too. The Wiz researchers wrote that the flaws had been present for decades, that exploiting them took a chain of steps "at the edge of what human researchers could practically achieve", and that "frontier AI models substantially reduced that barrier" (Krebs). The implication, which is mine rather than theirs: if old flaws get cheaper to turn into exploits, disclosure cadence goes up for every runtime. Plan for more days like this one, not fewer.

What 21 October cost you, by stack

StackPHP in the public request path?What you had to do on 21 OctoberWhat the next PHP CVE costs youSensible migration path
Classic WordPress or other PHP CMSYes, every pageUpdate PHP to a fixed release, restart every worker, or isolate the site until you couldThe same scramble, on the public siteGo headless, page type by page type
Headless WordPress (PHP backend behind a firewall, Next.js or Astro front end)No, backend onlyPatch the backend PHP; the public site kept servingA patch window on a locked-down host, no public outageStay; keep the backend allowlisted
Laravel or custom PHP appYesUpdate the runtime, restart workers, check what your host shippedThe same scramblePut a static or SSR front end in front and move page types out gradually
Static Jamstack (Astro, Next.js static export, Nuxt generate, Eleventy)NoNothing at the edge; check build tooling and any originNothing at the edgeStay
Next.js or Nuxt with SSRNo PHP, but a JavaScript server answers requestsNothing for PHP; its own runtime advisories applyA different runtime CVE with the same shapeMove pages that do not need per-request rendering to static output

Classic WordPress or other PHP CMS. Drupal, Magento, Joomla and WordPress all run on PHP, and Calif reports confirming pre-authentication remote code execution against specific versions of all four, per Tenable's FAQ. This row carried the whole cost of the day.

Headless WordPress. You patched, but on your schedule and behind a firewall. Editors kept editing. Visitors saw nothing.

Laravel or custom PHP. Same exposure as the classic CMS, with one wrinkle: nobody publishes a product advisory for your custom app, so you own the inventory. NCSC-NL's php2shell repository says custom PHP applications are not registered in its overview.

Static Jamstack. Nothing to do at the edge. If your build runs on a CI box, patch that when convenient, since it is not serving visitors.

SSR with Next.js or Nuxt. No PHP, no PHP2Shell. But a server process still answers requests, and React2Shell is the reminder that this process has its own bad days.

The 30/60/90-day plan

The goal is to take the public surface off PHP without rebuilding the editing experience.

  1. By day 30: list your page types (posts, landing pages, product pages, archives) and mark which genuinely need per-request logic. Confirm the PHP version on every host. Put a CDN in front of the origin and restrict the admin to an allowlist.
  2. By day 60: stand up an Astro or Next.js front end for the one or two highest-traffic static page types, built from the WordPress content API. Trigger a rebuild on publish. Add 301 redirects for every URL you move.
  3. By day 90: migrate the remaining page types that can be static. Close the PHP origin to public traffic so only the build process and your allowlisted editors reach it. Anything that truly needs a server (search, forms, accounts) goes to a small, named service you can patch on its own.

Taking the public surface off PHP without losing the CMS

You do not have to throw away WordPress to take PHP out of the request path. Headless WordPress keeps editors, content and the plugin workflow for writing and approvals, while Astro or Next.js serves the site. Editors keep the screen they know. Visitors get files from a CDN.

Migrate progressively. Pick a page type, rebuild it on the new front end, 301 the old URLs, and move on. The admin stays in PHP, behind an allowlist. The hosting pattern is simple: origin locked down, CDN in front, build on publish.

On framework choice: Astro for content sites, Next.js when the site needs an application layer, Nuxt for Vue teams. The deeper trade-offs are in the static site generator comparison and the headless WordPress guide.

On effort, using only the starting prices: WordPress to Astro migration is from $7k, headless WordPress development is from $10k, and a legacy CMS migration starts from $5k. If the site needs an application layer, WordPress to Next.js migration from $10k is the route that fits. Those are starting points, and the real figure depends on page types, integrations and how much editing workflow you want preserved. If you are weighing leaving PHP entirely, the leave-WordPress page covers that path.

The cheapest day to start this is not the day after an advisory. But the day after an advisory is when budgets get approved, so use it.

If you are staying on PHP for now

Some sites will stay put for a year or three. Fine. Do this in the meantime:

  1. Confirm the running PHP version is 8.5.12, 8.4.27, 8.3.36 or 8.2.35 (or a vendor backport you have read the notice for), and that every worker restarted. A package upgrade that leaves old workers running has fixed nothing.
  2. Check what your hosting control panel actually shipped, rather than what the dashboard says.
  3. Apply PHP's listed mitigations only as a bridge. The advisories say they do not make an installation safe, and the OOM advisory warns against RLIMIT_AS and RLIMIT_DATA, which trigger the vulnerable path.
  4. Plan the exit from 7.4 to 8.1 now. The PHP project no longer updates 8.1 and older, and you are depending on someone else's backport.
  5. Put the site behind a WAF while you patch. Cloudflare CTO Dane Knecht called its rules "a STOPGAP only", and Tenable notes they only cover traffic routed through Cloudflare.
  6. Watch for web shells and PHP files that should not exist. Wiz expects mass exploitation to install them, so check upload directories and recently modified files.

For sites that will remain on WordPress, WordPress security care from $150/month covers the ongoing patching and monitoring side.

The sharpest fact from 21 October is that the bug lived in the one place every PHP visitor touches, so the only sites with nothing to do were the ones where PHP never answered a public request. Static output is the strong version of that claim; headless WordPress still needed its backend patched, and SSR only swaps one runtime for another.

FAQ

Is WordPress affected by PHP2Shell?

WordPress itself is not the bug. The flaws are in PHP, and WordPress runs on PHP, so any WordPress site on an unpatched PHP was exposed. Tenable's FAQ, citing Calif, reports pre-authentication remote code execution confirmed against specific WordPress versions, which settles the question.

Does headless WordPress still need the patch?

Yes. The backend runs PHP, and an attacker who can reach it can reach the bug. What headless changes is exposure: the backend can sit behind a firewall or allowlist while a separate front end serves the public.

Is a Next.js or Astro site safer than PHP?

Static output from either is safer against this class of bug, because no server-side runtime answers requests. SSR is a trade: you swap PHP for a JavaScript server runtime with its own advisories, as React2Shell showed.

Does a WAF protect me?

Partly, and only for a while. Cloudflare called its rules a stopgap and they only cover traffic through Cloudflare. A WAF buys you time to patch. It is not a patch.

Should I take my site offline?

CERT-EU recommended considering isolation of internet-facing applications until a fix is available, and ENISA's joint statement says the same where immediate remediation is not feasible. If you can patch quickly, patch. If you cannot, isolation is a reasonable call.

Which PHP versions are fixed?

PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35, per the PHP advisories.

Are PHP 7.4, 8.0 and 8.1 patched?

Not with releases. The php.net advisory says 8.1 and older no longer receive updates from the PHP project, and the security team published minimal source-backport branches for 7.4, 8.0 and 8.1 instead. Ask your host or distribution whether it built them; either way, plan the upgrade.

Need this done, not just read?

Discuss your website Book 30 minutes