Post

PHP2Shell: The 14-Year-Old PHP Bug That Just Went Mass-Exploit

PHP2Shell (CVE-2026-109618/619): critical PHP RCE, CVSS 9.2, affecting PHP 5.4 to 8.5.11. Root causes, fake PoCs, exposure checks, detection, and patching.

PHP2Shell: The 14-Year-Old PHP Bug That Just Went Mass-Exploit

PHP2Shell: The 14-Year-Old PHP Bug That Just Went Mass-Exploit

On October 21, 2026, the PHP project shipped an emergency release for two memory-safety vulnerabilities in core PHP, with affected exploitation paths dating back to PHP 5.4.0. Both are rated CVSS 4.0 9.2, both enable unauthenticated remote code execution, and both live in the PHP runtime itself, not in Drupal, Laravel, Symfony, or any plugin you can disable.

Within hours of the patch, Cloudflare confirmed it was already seeing exploitation attempts and deployed emergency WAF rules. Security firm Sansec predicted mass exploitation within 15 minutes of the release. Wordfence put up a notice that its own services were temporarily unavailable while it implemented mitigations. Patchstack published a live alert and promised updates on social media. GitHub filled with repositories promising a “php2shell PoC.”

Most of those repositories are not what they claim to be.

This post breaks down what is actually verified, what the root causes are, why the public PoC ecosystem is a mess, and what defenders should do in the next hour.

Scope note: This is an analysis of public disclosures. I did not discover these vulnerabilities, and I did not develop or run an exploit against third-party systems. All testing guidance below is for systems you own or are explicitly authorized to test.

TL;DR

  • CVE-2026-109618 and CVE-2026-109619 are critical memory-safety vulnerabilities in core PHP.
  • Affected: PHP 5.4.0 through 8.5.11 (including 7.x, 8.0, and 8.1, which are end-of-life).
  • Patched: PHP 8.5.12, 8.4.27, 8.3.36, 8.2.35, released 2026-10-21 at 14:00 UTC.
  • Impact: unauthenticated RCE as the web worker, plus Heartbleed-style process memory disclosure.
  • The bug is application-agnostic. A minimal PHP script is enough. Drupal, Joomla, and WordPress were used as demonstrations; Magento, Laravel, Symfony, and custom PHP apps inherit the same exposure.
  • Public PoCs are mostly unverified, incomplete, or fake. Tenable explicitly stated it had not verified any public PoC works against either CVE.
  • Patch now. Configuration workarounds reduce specific attack paths but are not complete mitigations.

The leak, the panic, and the patch: a timeline

DateEvent
July 30, 2026OpenAI invites Calif to its Patch the Planet initiative; PHP is selected as a target.
August 1, 2026Calif’s audit surfaces the first OOM-fatal primitive and the first interruption point.
August 11, 2026Calif reports the first vulnerability to the PHP security team.
August 14, 2026Calif shares an RCE proof of concept with PHP.
August 15, 2026PHP confirms the PoC works as expected.
August 19–21, 2026Wiz discovers a parallel set of bugs and confirms RCE against Drupal and WordPress.
August 21, 2026Calif reports the second vulnerability.
August 24, 2026Wiz reports its findings to the PHP security team.
September 21, 2026PHP acknowledges Wiz’s reports and folds them into the coordinated fix.
October 20, 2026Details leak on Hacker News and social media. watchTowr confirms the bugs are real. PHP moves the release date forward.
October 21, 2026, 07:43 UTCCloudflare’s CTO reports early exploitation attempts and says WAF rules are deployed as a stopgap.
October 21, 2026, 14:00 UTCEmergency patches released.
October 21, 2026Public technical write-ups, advisories, and a wave of questionable PoC repositories appear.

The original disclosure was scheduled for October 22. The leak collapsed the embargo by a day. That matters because defenders got less preparation time, and because the public leak included a demonstration video and claims, not the discoverers’ exploit code.

The day the vendors went dark

The clearest sign that PHP2Shell was not a normal patch-day story came from the companies whose job is to protect everyone else.

On October 21, Wordfence’s own website stopped serving its normal interface. Visitors were greeted with one sentence:

“Wordfence services are temporarily unavailable as we implement mitigations for the currently unpatched PHP2Shell vulnerability. Thank you for your patience.”

Wordfence service notice: services temporarily unavailable while implementing mitigations for the currently unpatched PHP2Shell vulnerability.

The Wordfence notice, captured on October 21, 2026, before the patch was released.

Read that again. One of the best-known WordPress security vendors — a company that sells protection to a huge share of the WordPress ecosystem — put up a notice that its own services were temporarily unavailable while it implemented mitigations for a PHP bug that still had no patch.

Wordfence was not alone. Patchstack pushed an emergency alert to customers and published a live notice calling the flaw a critical RCE affecting WordPress, Drupal, Magento, Joomla, and Laravel; it promised live updates on LinkedIn, Facebook, and X. CERT-EU issued an advisory. Sansec warned that mass exploitation could begin within 15 minutes of the release. Cloudflare’s CTO said the company was already seeing early attacks and was updating WAF rules as new vectors appeared.

WordPress is where the alarm was loudest, because Wordfence and Patchstack are household names in that ecosystem. But the panic was not about WordPress. It was about the runtime underneath it — and underneath Drupal, Joomla, Magento, Laravel, Symfony, MediaWiki, shared hosting, control panels, container images, and the long tail of internal PHP tools nobody remembers deploying.

Behind the public alerts, the message to customers was blunt: this was not a plugin problem. Patching a CMS or framework would not save them. The runtime had to be patched.

On X, the reaction split into three camps. Researchers posted snippets, theories, and cautious confirmations. Defenders asked their hosts whether the patch was live yet. A third group promised a “one-click PoC” for a vulnerability that had not been fully patched. Screenshots of the leaked exploit tool traveled faster than the fix. Some claims were real. Many were not. The noise became part of the story.

The scale explains the panic. PHP is the runtime under a huge share of the web: Drupal, Joomla, Magento, Laravel, Symfony, WordPress, MediaWiki, shared hosting panels, appliance firmware, and countless internal applications. Wiz estimates that the vulnerable runtime affects around 70% of the internet. When the bug is in PHP itself, the blast radius is not “sites running a vulnerable plugin.” It is every PHP application that has not patched yet.

That is why the vendor notices read less like routine advisories and more like emergency broadcasts. For once, the people who usually write the “how to protect yourself” post were the ones scrambling to protect themselves.

What PHP2Shell actually is

“PHP2Shell” is the name of the exploit script built by Calif Research, not the name of a single bug. It chains multiple PHP engine weaknesses grouped under two CVEs.

 CVE-2026-109618CVE-2026-109619
ClassRCE via OOM corruptionRCE via stale global state
Root causelongjmp() OOM exits while request memory is partially initializedRequest-owned globals survive into a later request after the backing memory is freed
PrimitiveCorrupt process memory and execute codeUse-after-free, memory disclosure, controlled memory corruption
Confirmed impactUnauthenticated RCE as the web workerUnauthenticated heap UAF, memory disclosure, RCE
Easiest SAPIApache mod_phpApache mod_php, with a validated FPM path
Also relevantPHP-FPM, FrankenPHP, LSAPI, Windows PHPPHP-FPM, LSAPI, other SAPIs depending on producer/consumer reachability
CVSS v4.09.2 (AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N)9.2 (same vector)

The official advisories describe the shared pattern: PHP can leave internal state corrupted when a request ends abnormally. A later request, or the shutdown path of the same request, then trusts that state.

That is the sentence to remember. Everything else is exploitation detail.

Why this is a runtime bug, not an application bug

Most of the vulnerability research on this blog lives at the application layer: a plugin forgets a capability check, an AJAX handler trusts a request parameter, a REST endpoint leaks a private post. Those bugs matter, and they are common. I have written about several of them, including unauthenticated SEO Analyzer deletion in Rank Math and unauthenticated backup job execution in BackWPup. The same pattern shows up in every ecosystem.

PHP2Shell is a different category.

The vulnerable code runs below the application. Some paths execute before index.php is reached; others are triggered by ordinary application operations and consumed by the next request before initialization completes. A WordPress site with zero plugins, a Drupal site, a Laravel API, a Magento storefront, a custom PHP script with no framework, and a Symfony application are all exposed in the same way if they run on an affected PHP build.

That is why the Wiz demonstration targeted Drupal, Joomla, and WordPress container images: not because those applications are uniquely vulnerable, but because they prove the exploit does not depend on application code.

If you take one thing from this post, take this:

Updating your CMS does not fix PHP2Shell. Updating your framework or plugins does not fix PHP2Shell. Updating PHP does.

Root cause #1: CVE-2026-109618, when OOM becomes a write primitive

PHP uses longjmp() to abort the current context when an out-of-memory condition occurs. The intent is to fail the request quickly. The problem is when the failure happens.

During request startup, PHP temporarily exposes request-owned memory before it is fully initialized. If an allocation fails at that exact point, PHP does not properly account for the early exit. The request shutdown procedure can then read or branch on memory that is only partially initialized.

The Calif write-up traces one concrete pattern to _zend_hash_str_add_or_update_i:

1
2
3
4
5
6
7
8
idx = ht->nNumUsed++;
ht->nNumOfElements++;

p = ht->arData + idx;
p->key = zend_string_init(str, len, persistent); /* may zend_bailout() */

p->h = ZSTR_H(p->key) = h;
ZVAL_COPY_VALUE(&p->val, pData);

The table’s live count is incremented before the new bucket is fully initialized. If the key allocation fatals, the assignment to p->key and the zval copy are skipped. Teardown later walks the table up to nNumUsed and treats the stale bucket as live:

1
2
3
4
5
6
for (p = ht->arData; p != ht->arData + ht->nNumUsed; p++) {
    i_zval_ptr_dtor(&p->val);
    if (p->key) {
        zend_string_release(p->key);
    }
}

That is the core of the bug: publish state, perform fallible work, skip initialization, let teardown trust the false state.

The attacker’s job is to reach OOM at the right moment, within the limits PHP thinks it has.

How the attackers reach OOM inside post_max_size

The advisory and the Calif write-up describe several multipart parsing strategies:

  • A multipart request without a usable declared Content-Length can pass the initial post_max_size check and keep supplying decoded body data. Chunked HTTP/1.1 or lengthless HTTP/2 requests help here.
  • A single multipart part can contain an unrestricted number of headers. The in-memory representation can use far more memory than the wire representation.
  • Empty file controls (filename="") do not count toward max_file_uploads, but PHP still creates the full $_FILES metadata array: name, full_path, type, tmp_name, error, size.
  • Nested field names such as files[a][b][c]... multiply that work. Each empty upload record expands into multiple hash tables, buckets, and string keys.

Hundreds of these records can consume more than the default 128 MiB PHP heap while the HTTP body stays below the default 8 MiB post_max_size.

That is the alarming part: a request that looks like a bulky upload can exhaust PHP’s memory without ever violating the request-size limit.

Why mod_php is the worst case

The SAPI determines whether the corrupted state survives long enough to be consumed.

  • Apache mod_php unconditionally calls the request shutdown procedure, even when request startup failed. The invalid state is processed. The worker survives. This is the demonstrated application-independent RCE path.
  • PHP-FPM exits the child when request startup fails. That specific sequence does not survive. FPM still has a susceptible post-startup path, but it depends on application behavior.
  • FrankenPHP is described as analogous to mod_php and likely susceptible to application-independent attacks.
  • LSAPI models FPM, with application-specific exploits likely but not fully validated.
  • Windows PHP deployments are also affected.

PHP’s patch replaces longjmp() for OOM with a graceful soft OOM triggered at the next VM safe point, plus an immediate hard OOM using _exit() if memory consumption crosses a second, higher threshold. The shutdown path no longer sees invalid state.

Root cause #2: CVE-2026-109619, the ghost in the worker

CVE-2026-109619 is a cross-request use-after-free. It requires two roles:

  • Producer: a PHP operation stores request-owned state in a globally accessible location.
  • Consumer: a later request, handled by the same worker, reads, frees, branches on, or otherwise acts on that stale state before normal request initialization resets it.

The same worker condition is important. With multiple workers, the attacker may need many attempts. It does not remove the vulnerability; it only changes reliability.

The advisory documents three confirmed paths: INI diagnostics, fatal-error backtraces, and the session upload-progress lifecycle.

1. INI diagnostics before request initialization

A warning, notice, or deprecation emitted while applying configuration can consume stale error state. It does not need to be fatal, displayed, or logged.

With Apache mod_php, .htaccess and php_value / php_flag / php_admin_* directory configuration is applied before php_request_startup(). That makes early INI diagnostics a reachable consumer.

Examples include deprecated or invalid settings such as auto_detect_line_endings=1, legacy assert.* settings, legacy session settings, explicit mbstring.* or iconv.* encodings, invalid memory_limit values, and settings rejected under open_basedir.

FPM is conditionally exposed when nginx, Apache mod_proxy_fcgi, or another frontend sends PHP_VALUE / PHP_ADMIN_VALUE parameters that emit a diagnostic before request initialization. FPM pool values are applied at worker start, and .user.ini is applied after normal initialization, so those are not the same early consumer.

2. PHP 8.5 fatal error backtraces

With fatal_error_backtraces enabled, fatal error reporting stores backtrace state owned by the request. The reset is always missed during shutdown. A diagnostic on the next request reaches the stale backtrace before normal initialization resets it.

This is the strongest demonstrated case and the basis of the confirmed mod_php RCE.

3. Session upload progress

The session upload-progress feature can initialize request-owned session and decoder state before normal module initialization. It is enabled by default, and no application call to session_start() is required.

If parsing fatals after that state is published, the matching cleanup is skipped. Request shutdown frees the heap, but the worker’s session globals still point into it. A later multipart request can reach the upload-progress cleanup path before the normal session initialization resets the pointer.

The consumer is application-agnostic in practice: the attacker does not need the application to use sessions.

The FPM path: an old friend called unserialize()

The most interesting part of the Calif write-up is how FPM is exploited despite exiting workers on startup failure.

Once request startup succeeds, an ordinary fatal during script execution follows the normal shutdown path, and the worker can accept another request. The researchers looked for request-owned state that an application can leave dangling after startup.

They found it in PHP’s generic unserializer.

php_var_unserialize_init() allocates bookkeeping from the request heap and publishes it in BG(unserialize). If a fatal error interrupts a live decode, the matching destroy is skipped. The request heap is freed, but BG(unserialize).data still points into it, and the nesting level remains nonzero.

The next multipart request reaches session upload progress before normal request initialization. The stale nesting level makes the unserializer treat the dangling pointer as an active nested decoder. Ordinary request allocations can reclaim the freed storage first, giving the attacker control over the fake bookkeeping.

Serialized R: and r: references can follow selected entries in the reclaimed reference list. The destructor list provides a second primitive: a controlled native zval copy. The allocation helper checks used_slots + num > VAR_DTOR_ENTRIES_MAX but assumes the existing count cannot be negative. After reclamation, a negative used_slots selects memory before the intended array, and duplicate keys cause var_push_dtor_value() to write a genuine zval into that selected slot.

That is controlled memory corruption, and from there, RCE under both mod_php and FPM.

The application only needs to expose a reachable post-startup decoding operation such as session_start(), session_decode(), or unserialize(), including wrappers such as WordPress’s maybe_unserialize() or any framework-level decode helper. The decoded data can be legitimate. The attacker controls memory pressure around the decode.

The OPcache variant

Wiz also demonstrated a cross-worker type confusion in OPcache that folds under CVE-2026-109619. OPcache persists compiled PHP structures in shared memory and assigns indices into per-worker runtime-pointer maps. Workers coordinate those indices through a shared frontier. During zend_accel_script_persist(), PHP advances the process-local CG(map_ptr_last) frontier. It publishes the corresponding shared frontier only after persistence completes.

An allocation failure between those two events leaves the process-local advance in place and partially persisted objects referring to slots that the shared state still considers available. When another worker later reuses one of those slots for a different runtime object, stale shared metadata resolves to a pointer of the wrong type. The mismatch creates a cross-worker type confusion and corrupts shared OPcache state. Wiz used this primitive to build an unauthenticated RCE chain against WordPress.

Wiz groups this variant under CVE-2026-109619. The advisory’s broader “other error, exception, object and extension state” category covers the same class of state, and the complete patch resets it.

The memory leak that should worry you most

Not every exploitation path needs RCE.

PHP’s session upload-progress handling can publish a session ID in PS(id). If an out-of-memory error aborts the request before cleanup, PS(id) can point to freed memory. A later multipart request can reach the upload-progress cleanup path before normal session initialization resets that pointer. PHP then trusts the stale pointer as a live zend_string and URL-encodes the reclaimed heap contents into a PHPSESSID response cookie.

That is a Heartbleed-style remote memory disclosure, and it works even when the application never calls session_start().

The leaked memory can contain passwords, authentication tokens, API keys, and certificates belonging to other requests or the server itself. RCE is the loud outcome. Silent memory disclosure may be the more valuable one for an attacker.

The AI inflection point

The vulnerable code had been present for at least 14 years. What changed was the cost of turning it into working exploits.

The timeline is almost literary:

  • 2004: Stefan Esser discovers the first PHP memory-limit remote vulnerability. He considers it serious enough not to release the exploit publicly.
  • 2020: PHP bug #78876 reports that long variables in multipart/form-data can cause OOM and leave temporary files behind. It is not treated as an RCE.
  • 2024: OSTIF and Quarkslab audit PHP and rediscover a related OOM issue. Exploitation is not proven, so the bug is not fixed.
  • 2026: Calif, OpenAI, and Wiz revisit the pattern with frontier AI models. They find multiple interruption points, build working RCE chains, and report them to PHP.

Wiz says the work that previously would have taken a world-class team months was completed by a single researcher in a week, with models adapting the exploit to different PHP versions, configurations, and memory layouts.

The meta-lesson is not “AI finds bugs.” The meta-lesson is:

Exploit development cost has collapsed. Complexity is no longer a moat.

For defenders, that means the window between patch release and mass exploitation is shrinking. For researchers, it means the difference between a suspicious code pattern and a working exploit is no longer measured in months of specialist effort.

If you have read my notes on the security bugs I look for first when reviewing a codebase, this is the runtime-level version of the same lesson: trace the trust boundary, find the state transition, and ask what happens when the operation fails halfway.

The PoC circus: what is real and what is fake

This is the part nobody wants to say clearly, so I will.

Tenable’s FAQ states that code claiming to exploit these vulnerabilities is circulating publicly, but Tenable has not verified that any of it works against CVE-2026-109618 or CVE-2026-109619.

That is the correct epistemic starting point. A GitHub repository with a CVE number in the title is not evidence.

The patterns I have seen today:

  1. “Star this repo, PoC coming soon.” The repository contains a README, a disclaimer, and a promise. It contains no exploit. This is star farming, not research.
  2. Crash-only probes. A script sends a malformed multipart request, observes a 503 or a worker restart, and calls it a UAF. A crash proves the worker died. It does not prove control of execution.
  3. Conflating a separate logic bug with the CVE. Session upload-progress can be abused to place attacker-controlled data in a session file. If the application also has an LFI, that becomes RCE. That is a real chain, but it is not the root cause of CVE-2026-109618 or CVE-2026-109619. Publishing it as “the php2shell exploit” is misleading.
  4. Leaked “write-ups” that do not reproduce on released PHP. At least one public repository documents this explicitly: a claimed multipart UAF works on an ablated lab build but not on shipped PHP. That is exactly why negative controls matter.
  5. Fingerprint scanners sold as exploits. Matching a PHP/8.4 banner tells you the branch, not the patch level, and not whether the target is exploitable. Plenty of “PoCs” stop at the banner.

The real exploit is memory-corruption engineering. It requires heap grooming, version-specific offsets, and producer/consumer requests that reach the same worker. It is not a one-line curl.

If you want to publish a PoC that survives scrutiny, do what good vulnerability research does:

  • Show the vulnerable version and the patched version side by side.
  • Show the differential behavior, not just a crash.
  • Include negative controls.
  • Explain the exact state transition and why the patch changes it.
  • Do not run it against systems you do not own.

That is also the standard I apply in my own CVE write-ups: the PoC is the evidence, not the headline.

Am I affected? The five-minute check

You need to answer four questions.

1. What PHP version is actually running?

On a system you own:

1
2
php -v
php -r 'echo PHP_VERSION, PHP_EOL;'

On a web server, phpinfo() works only if you control the server and remove it afterward. A Server: header is not reliable; many deployments hide the PHP version, and reverse proxies can rewrite it.

2. Which SAPI is in use?

1
php -i | grep -i 'Server API'

Apache 2.0 Handler means mod_php. FPM/FastCGI means PHP-FPM. CGI/FastCGI can mean php-cgi or another FastCGI setup. FrankenPHP and LiteSpeed have their own signatures.

The SAPI matters because the exploit path and reliability differ. It does not change the patch recommendation.

3. Is the version below the fixed release?

BranchFixed versionStatus
PHP 8.58.5.12Supported
PHP 8.48.4.27Supported
PHP 8.38.3.36Supported
PHP 8.28.2.35Supported until 2026-12-31
PHP 8.1 and olderNo official packageEOL; source backports exist for 7.4, 8.0, 8.1

If you run 8.1 or older, “no patch available” does not mean “safe.” It means you need to upgrade or obtain a vendor backport.

4. Are you running PHP somewhere you forgot?

  • Container images that bundle their own PHP runtime.
  • Appliances and vendor products that embed PHP.
  • Shared hosting where you cannot see the host PHP version.
  • Windows IIS deployments.
  • Docker images pinned to an old tag.

Updating the host PHP does not necessarily update a bundled runtime inside a container or appliance. NCSC-NL explicitly calls this out.

What to do in the next hour

If you can patch

Patch. That is the only complete mitigation.

Upgrade to 8.5.12, 8.4.27, 8.3.36, or 8.2.35. For EOL branches, use the minimal source backports published by the PHP security team or a vendor-provided package. Restart PHP-FPM/Apache workers after the upgrade.

If you cannot patch immediately

PHP’s advisory is unusually direct: the following measures reduce exposure to specific attack paths but do not make an installation safe.

For CVE-2026-109618 (OOM corruption):

  • Set memory_limit=-1, preferably where applications cannot override it, such as php_admin_value in the FPM pool or Apache configuration. Verify that no pool, directory, or application setting restores a finite limit.
  • Set a conservative post_max_size; 8M is a reasonable baseline if the application does not need uploads, and larger values should be justified by actual upload requirements. Keep upload_max_filesize below post_max_size.
  • Enforce decoded request size, input field count, multipart part count, header count, and nesting depth at the web server or proxy. Cover requests without a usable Content-Length.
  • Use a container or cgroup memory limit as an availability boundary. Do not use RLIMIT_AS or RLIMIT_DATA; those can cause the vulnerable allocation failure.

For CVE-2026-109619 (stale global state):

  • Set session.upload_progress.enabled=0 and restart every worker. Apply it in php.ini, the FPM pool, or server configuration that takes effect before request parsing. Do not rely on ini_set() or .user.ini.
  • On PHP 8.5, set fatal_error_backtraces=0 and restart every worker.
  • Remove invalid or deprecated per-request INI settings that emit diagnostics before initialization. Hiding the diagnostic with error_reporting, display_errors, or @ does not help.
  • Fix attacker-reachable fatal errors and execution timeouts, including failures in callbacks, uploads, imports, and cleanup paths.
  • Consider pm.max_requests=1 if performance allows. It limits how long a worker can carry stale state.

WAF rules are temporary. Cloudflare shipped emergency WAF rules, and other vendors published out-of-the-box mitigations. The vendors themselves describe these as stopgaps. Do not treat a WAF as a patch.

If you are on shared hosting

Ask your provider, in writing, which PHP version your site runs and whether the October 21 security release has been applied. If they cannot answer, assume you are exposed. A control panel version number is not proof; the runtime can be different.

Detection: what exploitation looks like

PHP2Shell executes below the application layer, so application logs may contain little useful context. The signals are lower-level.

Wiz lists these indicators:

  • Unusual multipart requests followed by memory-limit errors or repeated HTTP 500 responses.
  • Unexpectedly large or malformed PHPSESSID response cookies (the memory-disclosure primitive).
  • Unexplained PHP worker churn.

PHP’s advisory adds an audit checklist:

  • Warnings, deprecations, and core errors repeated before target scripts run.
  • Diagnostics produced during extension or server cleanup.
  • Fatal, parse, and uncaught error messages.
  • Maximum execution time exceeded.
  • Failures restricted to routes, content types, files, callbacks, or plugin options controlled by an attacker.

For process-level detection, alert on PHP worker processes spawning shells or unexpected children:

  • php-fpm, php-cgi, httpd, apache2, or php-cgi.exe spawning sh, bash, cmd.exe, or powershell.exe.
  • Web-worker processes making outbound connections they do not normally make.

Enable log_errors=On, a functional error_log, and error_reporting=E_ALL during an isolated audit. Then review Apache, FPM, reverse proxy, and application logs.

One warning: absence of evidence is not evidence of absence. A failure can happen before PHP writes the message, and historical logs may have excluded deprecations or other relevant severities. Log review supplements a configuration and code-flow audit; it does not replace patching.

If you were exposed and unpatched, treat the system as potentially compromised. Memory disclosure does not leave the same traces as RCE, and the leaked data may include credentials and tokens.

FAQ

Is PHP2Shell a WordPress or CMS vulnerability?

No. It is a set of vulnerabilities in core PHP. WordPress, Drupal, Joomla, Magento, Laravel, Symfony, MediaWiki, and other applications were used as demonstration targets because they run on PHP. The vulnerable code is below the application.

Does updating my CMS or plugins fix it?

No. You must update PHP itself to 8.5.12, 8.4.27, 8.3.36, or 8.2.35. A CMS or plugin update cannot patch the runtime.

Is there a working public PoC?

Not one that has been independently verified across both CVEs. Tenable explicitly said it had not verified any public exploit code. Many repositories are incomplete, crash-only, or fake. The discoverers’ exploit script has not been published as a public repository.

Does disabling session.upload_progress fix everything?

No. It blocks the confirmed session-related paths for CVE-2026-109619. It does not fix CVE-2026-109618, and the advisory says other stale-state variants may exist. Patching is the only complete fix.

Does a WAF protect me?

A WAF can reduce exposure to known exploitation patterns. It is not a patch. Cloudflare and other vendors describe their rules as temporary mitigations. Attackers can adapt request shapes.

I am on PHP 7.4. Is there a patch?

Not an official one. PHP 7.4 is end-of-life. The PHP security team published minimal source backports for 7.4, 8.0, and 8.1, and some distributions or LTS vendors may provide packages. The sustainable fix is to upgrade to a supported branch.

Why is it called PHP2Shell?

“PHP2Shell” is the name of the exploit script built by Calif Research. The two CVEs are the underlying vulnerabilities. The name is branding for the exploit chain, not a vendor-assigned vulnerability name.

Was this exploited before the patch?

Cloudflare’s CTO said the company was already observing early attacks targeting the vulnerabilities before the patch release, and Cloudflare had deployed WAF rules. watchTowr and Huntress said they had not observed exploitation at the time of their initial statements. The situation is fluid. Assume exploitation is underway.

The bigger story

The patch is the urgent story. The meta-story is that a long-standing memory-safety pattern became a practical mass-exploitation primitive because the cost of exploit development dropped.

PHP2Shell is not interesting because a CMS was demoed. It is interesting because:

  • The bug is in the runtime, so application hardening does not help.
  • The default configuration is exposed.
  • The exploit is application-agnostic.
  • The memory-disclosure primitive leaks secrets even without RCE.
  • AI compressed the research and exploit-development cycle from months to days.

For defenders, the lesson is to shorten the patch window and stop treating “we use a secure framework” as a substitute for runtime patching. For researchers, the lesson is that a GitHub star count is not evidence, and a crash is not an exploit.

For everyone reading this on October 21: check your PHP version, patch it, and ignore the repositories promising a one-click shell.

References

Responsible disclosure and methodology

This article is an analysis of publicly available advisories, vendor statements, and the discoverers’ technical write-up. I did not discover CVE-2026-109618 or CVE-2026-109619. I did not develop a weaponized exploit for this post, and I did not test any third-party system.

All lab guidance is intended for systems you own or are explicitly authorized to test. If you maintain an internet-facing PHP deployment, verify your runtime version and apply the October 21, 2026 security release. If you cannot patch immediately, apply the vendor’s partial mitigations, monitor the indicators above, and treat any exposed, unpatched system as potentially compromised.

This post is licensed under CC BY 4.0 by the author.
Support my research