跪拜 Guibai
← Back to the summary

PHP2Shell: PHP Engine's 9.2 CVEs Are Live, and nginx Had Its Own Three 9.2s the Same Week

PHP2Shell Vulnerability Breakdown: Why WordPress and nginx Took the Hit

Hi everyone, I am 程序员天天困 (a programmer who is always sleepy).

Start with a 30-second self-check: open a terminal and run php -v.

If your version is older than 8.2.35, 8.3.36, 8.4.27, or 8.5.12, your site is in PHP2Shell's range. This is not a case of 'it might get scanned' - the patch has not even been formally released and people are already exploiting it.

Below, I will explain how the two CVEs break, why nobody was able to fix them for more than ten years, why nginx took collateral damage, and what to do tonight.

1. October 21: A release forced early by a leak

The strangest thing about this release is not the vulnerability itself, but the timeline being snapped in the middle.

At 15:00 UTC on October 20, the two CVE numbers started circulating on forums and social platforms, along with a wrong score: 10.0. The actual score is 9.2, and the release was also moved up a day.

The PHP team's first reaction was to stabilize the pace: at 19:30 UTC they sent out an early notice, moving the release from 22nd 14:00 to 21st 14:00. The reason was practical - distributors were still packaging, and an immediate release would leave many people without a simple upgrade path.

The real turning point was around 20:00 UTC. Calif accidentally posted a PoC video. It was deleted quickly, but copies had already spread.

On the 21st, the Netherlands' DigiD, MijnOverheid, parliament websites and Tweakers went offline, and several large services in Japan performed emergency maintenance. The patch was not out yet, but they stopped their systems first - that action says more than any CVSS score.

At 07:34 UTC, multiple hosting providers reported ongoing exploitation to Patchstack. Less than ten minutes later, Chainguard unilaterally lifted the embargo and released images with the patch. Two minutes after that, Cloudflare's CTO said they were seeing early attacks.

PHP Foundation postmortem: disclosure was interrupted, Chainguard released patched images six hours early

Time (UTC) What happened
10-20 15:00 CVE numbers leaked, with a wrong 10.0 score
10-20 19:30 PHP sent early notice, moving release from 22nd to 21st
10-20 20:00 Calif accidentally posted PoC video; copies spread
10-21 07:34 Hosting providers reported active exploitation
10-21 07:41 Chainguard released patched images early
10-21 13:30-14:00 Four versions released
10-21 onwards Mass scanning and in-the-wild exploitation

Officially confirmed affected runtime scope and four fixed versions

Between 13:30 and 14:00, the four versions went live. Sansec's warning was more direct: mass scanning would begin within 15 minutes of the official release. PoCs appeared on GitHub that same day, and one of them included a Docker lab with nginx + php-fpm 8.2.34.

What impressed me most was not the speed, but what it showed: once the PoC video leaked, the confidentiality window started narrowing; once patched images were up, you did not even need to guess the diff.

2. The root cause is not in index.php, it is in the last few milliseconds of engine cleanup

The most insidious thing about PHP2Shell is turning 'request failure' into an attack surface.

First, three terms, and the rest will be easier.

PHP2Shell: the name Wiz gave this combination exploit, meaning an attacker can execute commands on the server without authentication. Think of it as a crack in a building's foundation - whether WordPress or a custom backend is built on top, the crack is still there.

UAF (Use-After-Free): a block of memory has been freed, but the program keeps a pointer and continues using it. An analogy is a tenant who moves out without returning the key; the next tenant moves in, and you can still go through their things.

OPcache: PHP's mechanism for caching compiled script structures in shared memory, used by multiple workers. It is like a shared warehouse in a residential complex: anyone can put things in, and anyone can take the wrong one.

How exactly does it break? I will try to put it in plain language.

When PHP handles multipart requests, it parses RFC1867 at the 'request startup' stage, earlier than normal module initialization. The session upload_progress logic initializes session state in this very early window. The CVE-2026-109618 chain is: the decoder stores request-level accounting data in BG(unserialize), and at that point an allocation failure triggers a fatal error, destroying the request heap. But the deserializer's global state does not become invalid along with it. The next request handled by the same worker can treat memory reoccupied by the attacker as a live accounting structure.

In one sentence: cleanup was interrupted halfway, the tools were not put back, and the next shift came in and grabbed the wrong one.

Being able to corrupt is not enough; you also need to know where to write. Wiz found a separate memory leak primitive: in the same early window, PS(id) can point to freed memory; the next multipart request, during upload progress cleanup, treats that memory as a zend_string, URL-encodes it, and stuffs it into the PHPSESSID response Cookie. It leaks even if the application never called session_start(). This step removes the address uncertainty that ASLR gives you.

CVE-2026-109619 takes two other paths.

The first is fatal error backtrace. EG(last_fatal_error_backtrace) saves the call stack of the most recent fatal error, but it is not cleared when OOM destroys the request heap. The next request on the same worker only needs to output a diagnostic early to step on this dangling pointer.

The second is OPcache type confusion. zend_accel_script_persist() first advances the process-local CG(map_ptr_last) and only publishes the shared frontier after persistence is complete. If an allocation failure happens in the middle, the local state has advanced but the shared state still thinks the slot is empty; when another worker reuses it, it gets a pointer of the wrong type, and the shared OPcache is polluted.

Both paths cross requests and cross workers; restarting a single process is not necessarily clean.

The two CVEs sit on four concrete memory safety bugs, and the demo targets cover WordPress and Drupal

3. Why this bug could lie dormant for more than ten years

It is not that nobody saw the bug; it is that they saw it and could not fix it.

The PHP Foundation said frankly in its postmortem on release day: in 2024, PHP did its first external security audit in years, organized by OSTIF, executed by Quarkslab, and funded by the Sovereign Tech Agency. While fixing another issue from the audit report, a core developer funded by the Foundation first saw the beginnings of this OOM problem.

That year, some people speculated the OOM problem might be exploitable, but nobody produced a concrete path. Combined with more clearly understood bugs to fix, it was shelved.

Later, every fix route hurt: changing memory management could break backward compatibility; another approach would introduce a new DoS surface; some solutions required large numbers of third-party extensions to change, which could not be coordinated. The security team works by consensus; each option had costs, nobody could convince anyone else, and fixing stalled on and off for months.

The Foundation's own characterization of the process was: this was a structural failure, both in decision-making process and in staffing.

The real push came on August 11, 2026, when researcher Ryan A from Calif reported an unauthenticated RCE with the same root cause. On the same day OpenAI independently reported it, Wiz reported it on August 24, and OpenAI added a new vector on September 18. From August on, it became the entire team's highest priority, with people coordinating, validating, and writing patches through weekends and overnight.

Reading this, my feelings are complicated. We usually blame vendors for 'not fixing bugs', but the real situation is often a group of people torn between compatibility, performance, and security, and finally forced by an external report to pick one path. The security of open-source infrastructure is often not a technical problem; it is a scheduling and staffing problem.

4. Why changing WordPress or adding WAF rules cannot save you

Every plan that tries to 'block at the application layer' is inherently one step too slow.

This is the point I most want to emphasize: the vulnerable code is in the PHP engine. It does not need your routes, controllers, or middleware to nod. Under mod_php, an unauthenticated request can trigger it before application code runs, and the default configuration has already been demonstrated as full RCE; under PHP-FPM, the confirmed chain needs to go through an application code path - for example, session_start(), which is called by default. The bar differs, but neither requires an account.

Drupal users should not think this is far from them. Wiz's demo targets include drupal:latest, and Drupal officially put out PSA-2026-10-20 on the 20th, with a very direct title: 'Critical PHP update'. This is a PHP update, not a Drupal security release. Drupal's core update cannot fix it either, because the vulnerability is not in Drupal's code.

The same is true for WordPress. Automattic took part in coordination, but all it could do was issue guidance; no matter how you upgrade themes, plugins, or core, you cannot touch the PHP runtime. Joomla is also on the demo list. These three are just targets; with Laravel, ThinkPHP, or an inherited single-file backend, the conclusion is the same.

More hidden than the RCE is the memory leak. Wiz demonstrated reading memory without executing code: passwords, login tokens, API keys, and certificates left by other requests may all be URL-encoded into your own Cookie. On shared hosting, other sites in the same worker pool can also be affected - that is the most uncomfortable part of this kind of vulnerability: you may not even know you have been read once.

Some may ask: I use WordPress, can I not just wait for the official update?

No. PHP2Shell is in the PHP runtime, not in WordPress code. Upgrading core, themes, or plugins cannot touch it. You either upgrade the server PHP yourself, or ask your hosting provider to do it; if you use shared hosting, tonight is the time to ask, 'Is your PHP up to 8.2.35?'

There is one more class of 'mitigation' worth calling out: session.upload_progress.enabled=0, widely circulated online. It only closes the confirmed path of 109619 and has no effect on 109618. Datadog's statement is direct: this kind of community mitigation 'does not protect you.' Treating it as a lucky charm is more dangerous than not setting it, because you think you are safe.

WAF is the same. The Cloudflare rules block exploitation shapes already seen; changing encoding or tweaking chunking can bypass them. They buy time, but they do not stop variants and cannot replace an upgrade.

Plugins and scripts claiming to 'one-click fix PHP2Shell' are already appearing online. Do not install them. This unverified stuff has no validation; in the best case it does nothing, in the worst case it leaves a backdoor for you.

5. Is nginx really wronged?

nginx is both wronged and not wronged in this affair.

First, the wronged part: PHP2Shell does not touch a single line of nginx code. If you replace nginx with Apache or Caddy, PHP still gets hit. nginx is only the doorman here; it forwards multipart requests to php-fpm unchanged, and the real problem is the runtime behind the door. Those headlines saying 'PHP vulnerability affects nginx' have the causality reversed.

But the 'not wronged' part is more worth reading: in the same week, nginx itself had three CVSS 9.2s. This is not to make nginx take the blame for PHP2Shell; it is that it also had its own batch of 9.2s to fix that week.

On October 14, nginx 1.30.6 and 1.31.7 were released, fixing three memory-safety high-severity issues: a heap overflow when alias is combined with rewrite (CVE-2026-15176), a heap overflow when the map directive evaluates string expressions (CVE-2026-93479), and a UAF in the SSI module combined with AIO (CVE-2026-97205).

Singapore's CSA issued a dedicated alert on October 21, stating that all three are 9.2 and affect open-source nginx 1.30.5 and below, 1.31.6 and below, plus NGINX Plus R33-R37, Ingress Controller 5.6.0-5.6.3, and a whole string of products. Remote code execution requires ASLR to be off or bypassable, but crashing workers is reliable.

nginx's three 9.2s are a separate batch from PHP2Shell and were fixed a week earlier

So the real scenario is: your entry is nginx, and php-fpm is behind it. The door has its holes, and the house has its holes; two things landed in the same week. Only patching PHP but not nginx is like changing the door lock without fixing the window; only patching nginx but not PHP means the door is fixed but the hole in the house is still there.

By the way, the public PoC lab for PHP2Shell is nginx + php-fpm 8.2.34. That combination is so common that many people assume 'nginx in front is a bit safer.' It is not.

Decorative image

6. Two other 'explosions' on the same day, but very different in nature

October 21 was a patch day, but not everything on it was an RCE.

On the Node.js side, CVE-2026-111497 is an out-of-bounds write in the Brotli decoder. The official classification says it can lead to remote code execution, rated high. Fixed versions are 22.23.4, 24.22.1, and 26.12.1. The same security release also included dependency updates for c-ares, OpenSSL, Undici, and several TLS hostname verification issues. If you run Node services, take a look at node -v: the 22, 24, and 26 lines are all in range.

Node.js security release: Brotli decoder out-of-bounds write CVE-2026-111497, rated high

On the Go side, CVE-2026-111943 has been written by many headlines as 'Go also has an RCE.' It does not. It is crypto/x509 failing to properly block known-compromised intermediate certificates during Certificate.Verify, CWE-299, with Red Hat's CVSS 3.1 score at 6.8. The consequence is a man-in-the-middle attack: attackers can use certificates signed by those intermediate CAs to impersonate legitimate servers. Affected versions are before 1.26.10 and before 1.27.3.

How far apart are they? An analogy: PHP's case is a wall that has been breached; Go's case is a door lock that should have been changed and was not. Both are dangerous, but the defensive moves are completely different. The first needs an overnight runtime upgrade; the second needs a review of the trust chain and certificate pinning policy, and a pass over Verify error handling.

I put all three in one table to help you prioritize:

ID What it is Worst outcome Severity
CVE-2026-109618 / 109619 PHP engine memory corruption Unauthenticated RCE 9.2 (v4.0)
CVE-2026-111497 Node Brotli decoder out-of-bounds write RCE high
CVE-2026-111943 Go x509 certificate chain validation missing MITM impersonation 6.8 (v3.1)

7. Four things you can do tonight

Mitigations only reduce harm, not grant immunity; PHP officially wrote this in the announcement.

First, confirm the version before talking about anything else. The targets are one of these four:

Branch Fixed version Notes
8.5 8.5.12 Current mainline
8.4 8.4.27
8.3 8.3.36
8.2 8.2.35 EOL 2026-12-31; after upgrading, plan your next move

PHP 8.1 and earlier no longer get official releases, but this time minimal source backport branches for 7.4, 8.0, and 8.1 were provided. Distributions and LTS vendors may also have backport packages; ask your vendor instead of guessing. Also, the volunteer repository rpms.remirepo.net said on the same day that it will have no updates; people using it need to switch sources or compile themselves. Docker users should rebuild images after upgrading; do not just change the tag without pulling again, because the local cache may still be on the old digest.

Second, know how to set memory_limit and post_max_size. The official temporary mitigation for 109618 is to set memory_limit to -1, preferably in the FPM pool or Apache's php_admin_value, so applications cannot change it back. Note that this only removes the controllable memory ceiling; the underlying allocation failure can still reach that path.

Also keep post_max_size small: 8M for sites that do not need uploads, 32M for sites that do, and upload_max_filesize below it. If you have the conditions, use containers or cgroups as the availability boundary; do not use RLIMIT_AS or RLIMIT_DATA to 'limit memory' - that is exactly what triggers the fatal allocation failure.

Third, do not treat session.upload_progress.enabled=0 as a panacea. It must take effect in php.ini, the FPM pool, or server configuration, and then all workers must be restarted; ini_set() and .user.ini do not count. It only covers part of 109619; 109618 will still hit.

Fourth, check logs, then think about whether to rotate keys. Successful exploitation does not necessarily crash the process; do not wait for a crash. Signals worth looking for: abnormally large form submissions, memory exhaustion during request initialization, repeated 500s, an unusually large or malformed PHPSESSID response Cookie, unexplained PHP worker restarts, and web processes suddenly spawning a shell.

If any of these signals have appeared, or if your machine was exposed from the 21st until the time you patched, patching alone is not enough - assume database passwords, API keys, and session keys have already been read, and rotate what needs rotating. The UK NCSC's advice is harsher: for systems that cannot be fixed, consider taking them offline first.

Cloudflare has enabled WAF rules for all plans (including free) against known exploit vectors; if you have a CDN, turn it on. But it has a precondition: traffic must all go through Cloudflare. Once the origin IP is exposed, the rules are decorations. If you really want to survive, you still have to upgrade the version.

8. My judgment

To be honest, the thing that chilled my spine is not the 9.2, but Wiz's statement: these defects have lain in the code for more than ten years, and what turned 'unexploitable' into 'exploitable' was frontier models.

In the past, risk assessment had an implicit premise: exploit complexity is itself a defense. A vulnerability requiring precise memory layout control, repeated debugging, and per-target adaptation could only be handled by elite teams, so 'defer the fix' was acceptable. PHP2Shell broke that premise. Wiz said one researcher in one week did what used to take a team months, demonstrating WordPress, Drupal, and Joomla as three different targets.

For ordinary developers, the concrete meaning is: 'this vulnerability is too hard to exploit' is no longer a reason that can be written into a risk assessment.

One more number is worth remembering. CybelAngel scanned 2.24 million PHP hosts exposed to the public internet, and 59% of them run versions that will never get another patch.

Scanned host statistics

These people are not unaware; they cannot move - old systems, old CMSes, hosting providers that do not upgrade, nobody dares to touch them. This long tail will drag on for a long time, long enough that when the next similar thing explodes, they will still be there.

So my advice is plain: tonight, check the version number. Upgrade if you can; if you cannot, apply the official mitigation steps and put migration on your schedule. PHP2Shell will not be the last one. Do not wait for the next October 21.


I am 程序员天天困, still sharing programming content. If you found this useful, do not forget to like, bookmark, and follow. Feel free to comment: what PHP version are your projects stuck on right now - can't upgrade or afraid to?