PHP2Shell: Critical Remote Code Execution in PHP Itself (CVE‑2026‑109618 and CVE‑2026‑109619)

On 21 October 2026, PHP shipped emergency releases for two critical flaws that let an unauthenticated attacker run code on a web server. The flaws are in the PHP engine, not in WordPress, Drupal or any plugin. Researchers named the pair PHP2Shell.
The PHP security team rates both CVE-2026-109618 and CVE-2026-109619 at CVSS v4.0 9.2. They affect every PHP installation that serves web requests, and exploitation started before the patch existed: Amazon's honeypots recorded the first attempt on 17 October, four days before disclosure.
The short version:
- What it is: two memory-safety bugs in how PHP recovers when a request fails partway through, most easily by running out of memory.
- Impact: unauthenticated remote code execution, plus a leak of worker memory back to the attacker.
- Fixed in: PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35, or your distribution's patched package.
- End of life: minimal source backports for 8.1, 8.0 and 7.4. Nothing from PHP for 7.3 and older.
- Status: exploited in the wild, with public proof-of-concept code.
This post covers how the bugs work, how they became public early, who gets a fix, and what to do if you cannot patch within the hour.
Two bugs, one trigger
Both CVEs start the same way: an attacker makes a request fail partway through. The easiest way is to push it past memory_limit. What goes wrong afterwards is different for each CVE.
Two bugs, one trigger
An attacker forces a request to fail partway through, most easily by pushing it past memory_limit with an ordinary multipart upload.
CVE-2026-109618Out-of-memory corruption
-
PHP is building request data
-
Fails
Out of memory: PHP stops while the data is only half-initialized
-
Cleanup treats the half-built data as valid: memory corruption, code execution
CVE-2026-109619Stale state
-
Request A: PHP keeps a pointer to request memory in a long-lived variable
-
Fails
The request fails: its memory is freed, the pointer is not cleared
-
Request B on the same worker uses the stale pointer: leak, corruption, code execution
CVE-2026-109618 is out-of-memory corruption. PHP's advisory says out-of-memory handling can interrupt PHP while internal request data is only partly initialized. Cleanup then treats the invalid data as valid.
CVE-2026-109619 is stale state. PHP workers live across many requests: an Apache mod_php child or a PHP-FPM child handles one request, then the next. While a request runs, PHP keeps pointers to request-owned memory in long-lived variables. If the request dies, its memory is freed but the pointer is not cleared. The next request on that worker can use the pointer before PHP resets it, so freed memory is treated as live data. If the attacker controls what lands in that memory, the result is a leak, corruption or code execution.
Wiz Research described four concrete bugs and grouped them under the two CVEs. Its grouping follows the code paths, not PHP's root-cause descriptions, so the two don't line up one to one:
| Bug (Wiz's grouping) | Wiz lists it under | What it gives an attacker |
|---|---|---|
| Upload-progress unserializer use-after-free | CVE-2026-109618 | Memory corruption and code execution |
| Remote process-memory disclosure | CVE-2026-109618 | Unauthenticated read of worker memory |
| Fatal-error backtrace use-after-free (PHP 8.5) | CVE-2026-109619 | Memory corruption and code execution |
| OPcache type confusion across workers | CVE-2026-109619 | Corrupted shared state and code execution |
For defenders the CVE labels matter less than the result: the patched releases fix both CVEs.
Calif, which found the first paths, showed how to push a request over memory_limit on demand with ordinary multipart uploads. PHP checks post_max_size against the declared Content-Length, not against what it actually allocates. Empty-filename parts and nested field names let a body under the default 8 MB limit exhaust memory. That makes the trigger reliable.
The SAPI matters. PHP lists PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed as affected, and considers the risk highest for mod_php. On mod_php, a failure during request startup leaves the worker alive with stale state, and the next request picks it up. PHP-FPM kills a worker whose startup fails, so the attacker needs a fatal error after startup has finished, plus a later request to the same worker.
The leak is a separate problem
The bugs can also leak memory. According to Sansec, PHP's upload-progress handling can return the contents of freed worker memory to the client inside a PHPSESSID response cookie. Sansec says that memory can contain passwords, session tokens, API keys and TLS private keys from other requests. Treat that as the worst case: what is in reach depends on how the server is set up.
Wiz showed the leak does not depend on the application. Even a "Hello World" script on an affected PHP build is exposed. A leak like this also helps an attacker make the code-execution chain reliable.
What an attack looks like
Put together, the public research describes a chain that needs no login and no particular application. The figure below simplifies it and pairs each attacker step with what the server records, which is where defenders can catch it. It is an illustration, not a recorded incident.
An attack, step by step
Placeholder: this figure is being drawn.
Steps 2 to 4 all happen inside PHP, before any application code runs, which is why application logs see so little. The first traces are in the web server log and the PHP error log. Step 5 is ordinary post-exploitation and looks like any other web server compromise.
Why patching your application does not help
The vulnerable code runs before your application does.
Where PHP2Shell sits in the request path
-
Internet
one HTTP request
-
Web server
mod_php, PHP-FPM, php-cgi, FrankenPHP, LiteSpeed
-
PHP request startup
multipart parsing, session upload progress
Bug triggers here -
Your application
WordPress, Drupal, Joomla, a “Hello World” script
Never has to run
Patching a plugin or hardening the app does not fix it. The fix is a PHP update on the server.
PHP parses multipart bodies and starts session upload-progress handling during request startup, before it loads a single line of application code. A "Hello World" script on a vulnerable PHP build is exposed, and so is every application on it.
Calif confirmed unauthenticated code execution against WordPress, Drupal, Joomla, MediaWiki, Roundcube, Magento, phpMyAdmin, Nextcloud, Laravel, Symfony and WHMCS. Wiz showed its exploit chain working against the latest container images of WordPress, Drupal and Joomla. These are examples, not preconditions.
Two practical consequences follow:
- Plugin updates and application hardening do not fix it. The fix is a PHP update on the server.
- Maintenance mode does not help. PHP parses each request before the application checks the maintenance flag.
Twenty years in the making, three months in disclosure
The root of this story is old. Calif's write-up traces it back to 2004, when Stefan Esser published the first PHP memory-limit remote vulnerability. In 2020, bug report #78876 described long variables causing out-of-memory conditions, and nobody treated it as a security issue. In 2024, an audit by OSTIF and Quarkslab rediscovered the same out-of-memory problem, but exploitation was never proven, so it was not fixed.
In 2026, OpenAI invited Calif into its Patch the Planet initiative, and PHP stood out as a high-impact target. Calif found multiple code execution paths on 1 August and reported the first bug to PHP on 11 August. Wiz found its first bug independently on 19 August and reported on 24 August. PHP acknowledged the reports on 21 September and started on a fix.
From first report to broad exploitation
2026. Milestones, not to scale. Times in UTC.
Research and coordinated disclosure
Calif finds multiple code execution paths
Calif reports the first bug to PHP
Wiz finds its first vulnerability
Calif reports the second bug
Wiz reports to the PHP team
PHP acknowledges and starts on a fix
The final week
- Attack
First exploitation attempt in Amazon's MadPot honeypots
Details leak; watchTowr confirms; PHP pre-announces a release
- Attack
First public proof-of-concept exploits; more attempts from other actors
Cloudflare default WAF rules live on all plans
Fixed releases and full disclosure
The fix was scheduled for 21 October at 14:00 UTC. On 20 October details leaked, watchTowr confirmed the flaws were real, and PHP pre-announced the release. For about a day, everyone knew a critical PHP bug was coming and nobody had a patch.
Some operators chose to go dark. On 21 October several Dutch sites, including Tweakers, the House of Representatives, the NCSC and Security.NL, went offline as a precaution. Wikimedia took down services including its Phabricator instance while it applied the patches.
Exploitation started before the patch
Amazon Threat Intelligence found the first exploitation attempt in its MadPot honeypot data, dated 17 October, from an address in a commercial anonymization network. The vulnerability was still under embargo. Amazon says it remains unclear how the actor got exploit information, and that the activity suggests a threat actor.
On release day, the pace changed:
- Amazon saw multiple proof-of-concept exploits from around 02:00 UTC on 21 October, twelve hours before the embargo lifted.
- Between 02:00 and 08:00 UTC, Amazon recorded more attempts. The payloads and infrastructure varied, which suggests multiple actors. Amazon notes it is unclear whether these attempts would have succeeded.
- Cloudflare's default WAF rules went live for all plans at 03:02 UTC, and Cloudflare reports seeing attack traffic against its customers.
- Amazon's assessment after release: PHP2Shell is now being broadly exploited in the wild.
Vendors' visibility differs this early. For investigation, use the earliest credible date: 17 October.
The criminal side reacted too. SOCRadar reports that underground forums including BreachForums and Dread took themselves offline as a precaution. It also saw forum threads asking for working exploits, with one poster complaining that the public ones did not work on every version.
A warning about public PoCs. JFrog found GitHub repositories posing as PHP2Shell exploits that were pranks, one of them a script that changes the victim's wallpaper. A fake today can be a real implant tomorrow. Do not run exploit code from unvetted repositories against anything you care about.
Indicators
Amazon published the source addresses it saw. They include shared anonymization infrastructure and are not exclusive to this activity, so investigate matches rather than blocking them blindly.
| IP address | First seen |
|---|---|
103.216.220[.]92 |
17 Oct 2026 |
31.223.184[.]166 |
21 Oct 2026 |
37.19.200[.]5 |
21 Oct 2026 |
134.209.159[.]64 |
21 Oct 2026 |
103.114.163[.]188 |
21 Oct 2026 |
95.217.98[.]37 |
21 Oct 2026 |
Who gets a fix
Who gets a fix
Official PHP releases as of 22 October 2026
| Branch | Fix from PHP | Supported until |
|---|---|---|
| PHP 8.5 | 8.5.12Fixed release | 31 Dec 2029 |
| PHP 8.4 | 8.4.27Fixed release | 31 Dec 2028 |
| PHP 8.3 | 8.3.36Fixed release | 31 Dec 2027 (security fixes only) |
| PHP 8.2 | 8.2.35Fixed release, ends this year | 31 Dec 2026 (end of life) |
| PHP 8.1, 8.0, 7.4 | Minimal source backportBuild from source or ask your vendor | End of life |
| PHP 7.3 and older | None from PHPUpgrade or vendor backport | End of life |
Distribution packages (Debian, Ubuntu, RHEL and others) carry the fix under their own version numbers.
Four branches received fixed releases. PHP 8.1, 8.0 and 7.4 are end of life, but given the severity, PHP's security team published minimal source backports for them, for people who build from source or have no LTS vendor. PHP 7.3 and older get nothing from PHP.
The exposed population is large. SOCRadar, citing Shodan, counts about 884,000 internet-facing hosts on PHP 8.1 or older, and another 740,000 that run PHP without showing a version.
Internet-facing PHP hosts with no upstream fix or no visible version
Unknown is not patched. Treat these as exposed until checked.
That second number matters. A missing version banner does not mean a patched server. Treat unknown as unpatched until someone has checked.
A few traps to watch for:
- Distribution packages. Linux distributions received the patches in advance and often ship them under their own version numbers, so a patched Debian or Ubuntu server may not report 8.3.36. Check your distribution's security advisory for the fixed package version.
- Remi's repository. Many RHEL-based servers install PHP from
rpms.remirepo.net. The volunteer-run repository did not publish the fix on release day. Use your distribution's packages or build from source. - Bundled PHP. Vendor appliances, Windows software and hosting control panels such as cPanel and Plesk ship their own PHP. Inventory those too. NCSC-NL keeps lists of software with a PHP dependency.
- Your next upgrade. PHP 8.2 reaches end of life on 31 December 2026. If you are choosing a target today, 8.4 has security support until the end of 2028 and 8.5 until the end of 2029.
Patch, then restart every worker
Patching is the only fix. Amazon puts it plainly: upgrading is the only complete remediation for both CVEs, and no configuration-only workaround fully addresses both.
Update to 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or your distribution's fixed package, internet-facing servers first. Then restart PHP-FPM or Apache. A package update does not help while the old workers are still running the old build.
# Debian/Ubuntu example with PHP 8.3 and FPM. Adjust for your version and distro.
sudo apt update && sudo apt install --only-upgrade php8.3-fpm php8.3-cli php8.3-common
sudo systemctl restart php8.3-fpm # mod_php: sudo systemctl restart apache2
# Compare the installed package version with your distribution's security advisory
dpkg -l 'php8.3*' | grep ^ii
# Confirm the workers started after the update
ps -o pid,lstart,cmd -C php-fpm8.3
Check the version in the context that serves requests. The command-line php and PHP-FPM can be different builds with different php.ini files. In containers, rebuild and redeploy the image rather than patching in place.
Then hunt for earlier compromise and rotate exposed secrets, as described under Detecting exploitation. A patch stops new attacks; it does not undo old ones.
If you cannot patch within the hour
Work down this list. Only step 1 is a fix; every step after it is partial.
If you cannot patch within the hour
Work down the list. Only step 1 is a fix.
-
Update PHP and restart every workerComplete fix
8.5.12, 8.4.27, 8.3.36 or 8.2.35, or your distribution's fixed package
-
Edge WAF rulesPartial
For example Cloudflare's default managed rules. An extra layer, not a fix
-
Reject multipart/form-data at the web serverPartial
Every known trigger is a multipart request. Breaks uploads
-
PHP settingsPartial
session.upload_progress.enabled=0,memory_limit=-1,post_max_size,fatal_error_backtraces=0on 8.5. Each covers only some paths -
Take it offline, or allow only GET and HEADStopgap
Stops exposure, costs availability
1. Update PHP and restart every worker. See above.
2. Edge WAF rules. Cloudflare turned on default managed rules for free and paid plans. They use a new capability called full transaction detection, which combines signals from the request and the response in the same HTTP transaction. Professional, Business and Enterprise customers should confirm the Managed Ruleset is deployed in block mode. A WAF is an extra layer. The vulnerable code runs inside PHP, so it cannot be the only one.
3. Reject multipart/form-data at the web server. Every known trigger is a multipart request, and PHP parses that body before your code runs. Amazon recommends blocking multipart uploads on deployments that cannot be patched immediately. It also breaks file uploads, and some applications submit checkout or cart forms as multipart, so test after you apply it.
# nginx: inside every server block that passes requests to PHP
if ($content_type ~* "multipart/form-data") {
return 403;
}
# Apache: inside every VirtualHost that serves PHP
RewriteEngine On
RewriteCond %{HTTP:Content-Type} multipart/form-data [NC]
RewriteRule ^ - [F]
4. PHP settings. PHP's advisory lists configuration changes that cut off specific attack paths:
- For CVE-2026-109618, set
memory_limit=-1in a way applications cannot override, such asphp_admin_value. Setpost_max_size=8M, or32Mif you need uploads, and keepupload_max_filesizebelow it. Limit request size, field count and multipart part count at the web server or proxy. Enforce memory limits with container or cgroup controls, notRLIMIT_ASorRLIMIT_DATA, which can cause the vulnerable allocation failure. - For CVE-2026-109619, set
session.upload_progress.enabled=0. This blocks the confirmed session paths, but not other variants. On PHP 8.5, also setfatal_error_backtraces=0, which blocks the confirmed fatal backtrace path only. - Set these in
php.ini, the FPM pool or the server configuration, not withini_set()or FPM.user.inifiles, and restart every worker afterwards. - JFrog adds
pm.max_requests=1, which breaks the same-worker requirement at a heavy performance cost.
; PHP-FPM pool file, for example /etc/php/8.3/fpm/pool.d/www.conf
php_admin_flag[session.upload_progress.enabled] = off
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M
; PHP 8.5 only
php_admin_flag[fatal_error_backtraces] = off
Be honest about what this buys you. The upload-progress switch does not touch the OPcache or backtrace bugs. memory_limit=-1 lets a single request consume all server memory, which hands attackers a denial-of-service lever. Each measure is partial on its own.
5. Take it offline, or allow only GET and HEAD. CERT-EU's advice for systems that cannot be patched is to isolate internet-facing systems from public networks until the fix is applied and verified. If you cannot go dark, have the web server reject every other method. Visitors can still browse, but cannot log in or order.
if ($request_method !~ ^(GET|HEAD)$) {
return 503;
}
Detecting exploitation
The code runs below the application, so application logs may say very little. Hunt at the web server, the PHP error log and the process level.
Start with the request and its side effects:
- Multipart POST requests containing
PHP_SESSION_UPLOAD_PROGRESS, especially ones with no real file part. - Bursts of out-of-memory fatals and HTTP 500 responses following multipart requests from the same source.
- Unusually large or malformed
PHPSESSIDvalues inSet-Cookieresponse headers. This is the memory leak showing up on the wire. - Unexplained PHP worker churn, such as workers dying and respawning.
Then look for what an attacker does next:
php-fpm,php-cgi,httpdorapache2spawning shells, script interpreters,curlorwget.- New files in the web root, new cron jobs or scheduled tasks, and unexpected accounts.
Keep the raw request logs. Investigate from 17 October, not from the disclosure date.
Rotate secrets on anything that was exposed. The memory leak can return credentials and keys, and nothing in the patch un-leaks them. If a server was reachable and unpatched, rotate its database credentials, API keys and session secrets, and where memory exposure is plausible, its certificates.
The bug was old. The exploit was not.
The vulnerable pattern sat in PHP for years. Memory-safety flaws like this are hard to turn into reliable exploits, because every PHP version, configuration and application changes the memory layout the exploit depends on. What changed in 2026 is the cost of that work.
Wiz says frontier AI models helped its team develop and adapt working exploits across multiple applications in hours, and that a single researcher finished in a week what would once have taken a world-class team months. Amazon draws the defensive conclusion: when exploits arrive within hours of disclosure, a 7-day or 30-day patch SLA for internet-facing systems assumes a delay that no longer exists.
PHP2Shell also shows the limit of an embargo. Coordinated disclosure is still the right model, but it only protects defenders until someone else finds the same bug. Here, someone did.
Three habits follow from that:
- Know where PHP runs. The hardest part of this event is the inventory: every internet-facing host, the PHP build it reports, and which ones sit on end-of-life branches. That is the problem Attack Surface Management is built to answer, and it is why the 740,000 hosts with no visible version are the ones to worry about.
- Measure patching in hours for critical flaws on exposed systems, and have a pre-agreed plan for taking something offline.
- Detect behavior, not just signatures. The first exploitation of a bug like this arrives before any signature exists. A web server spawning a shell is suspicious regardless of the CVE.
We will keep tracking exploitation, new variants and changes to the public proof-of-concept code. Wiz notes there are other tracked and untracked variants of this pattern, and says PHP's fix addresses it at the core, so the patched releases cover them too. Configuration workarounds are another matter: PHP says they block only the confirmed paths.
References
- PHP security release announcement, 21 October 2026
- PHP supported versions
- Wiz Research: PHP2Shell technical write-up
- Calif: A tale of two PHP remote vulnerabilities
- Amazon Threat Intelligence: PHP2Shell exploitation prior to public disclosure in MadPot
- Cloudflare: protections against the php2shell vulnerability
- Sansec: PHP2Shell critical PHP RCE
- JFrog Security Research: PHP2Shell, all you need to know
- SOCRadar: PHP2Shell, CVE-2026-109618 and CVE-2026-109619
- CERT-EU Security Advisory 2026-018
- NCSC-NL: PHP2Shell operational information
- Wikimedia: incident report, 2026-10-21 PHP security update