Guide · Backend
PHP's October 2026 PHP2Shell Bugs Explained: How to Patch and Verify
21 min readUpdated By Saket Bhatnagar
Researched and drafted with AI, reviewed and edited by Saket Bhatnagar.
In short
- PHP2Shell is the name Wiz Research gave to two critical bugs in PHP itself: CVE-2026-109618 and CVE-2026-109619 (CVE ids are public bug numbers). They can let an attacker run code on your server without logging in.
- The fix is a PHP update, not an app or plugin update. Move to PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35, released on 21 October 2026, or to your Linux vendor's patched package.
- The main trigger for both bugs is an emergency stop, such as a request running out of memory. PHP jumps straight to its clean-up code and later trusts data that is half-built or already freed.
- Check the package, not just php -v. Ubuntu 24.04's patched build still prints PHP 8.3.6, so look for the CVE ids in the package changelog, then restart PHP.
- Beginners can stop after the patching section. Engineers then get a server-by-server risk table, what each stopgap misses, and a new, undocumented memory_hard_limit_percent setting that the fix added.
What goes wrong when the security bug is in PHP itself?
With PHP2Shell, every PHP site on an unpatched server is at risk, and updating your app or plugins does not help. The broken code sits inside PHP, and some attack paths fire before your own code even starts.
Picture a common setup. A small online store in Jaipur sells block-printed bedsheets and gets about 2 lakh (200,000) visits a month. It runs a PHP shopping cart on one Ubuntu 24.04 server, with Nginx in front and PHP-FPM running the PHP code. The store is made up, but the bugs and every command in this guide are real.
On 21 October 2026, php.net told every PHP user to update now. The store's developer keeps the cart and its plugins current, so the first instinct is to run the plugin updater. That changes nothing here.
This guide follows that one server from the alert to a verified fix. You will see what the two bugs are, who is really at risk, how to check and patch, and what to do if you cannot patch today. In a hurry? Jump to the check and patch sections, then come back for the why.
Which words do you need before reading about PHP2Shell?
You need six terms. Each one gets a plain meaning and its place on the store's server.
- Remote code execution (RCE): an attacker on the internet makes your server run commands they choose. Here, those commands would run as the web worker's user account.
- Worker: a long-running PHP process that handles one request, then the next, then the next. The store's PHP-FPM keeps a small pool of workers running all day.
- SAPI: the way PHP plugs into the thing that runs it. PHP-FPM (a separate pool of PHP processes), mod_php (PHP inside Apache) and command-line php are different SAPIs.
- memory_limit: the most memory one PHP request may use. Go over it, and PHP stops the request with a fatal error, an error that ends the request at once.
- Use-after-free: a program frees a piece of memory but keeps a pointer to it and uses it later. By then that memory may hold someone else's data.
- CVE, GHSA and CVSS: a CVE id is a public bug number, and a GHSA is the GitHub advisory page for it. CVSS is a severity score from 0 to 10.
What is PHP2Shell (CVE-2026-109618 and CVE-2026-109619)?
PHP2Shell is a pair of critical PHP bugs that can give an attacker remote code execution without any login. Wiz Research, one of the teams that reported them, coined the name. php.net simply calls them two critical vulnerabilities.
The PHP security team describes them in two GitHub advisories:
- CVE-2026-109618, RCE via OOM corruption (GHSA-w8hw-w9fx-j79w). OOM means out of memory. If memory runs out while PHP is still setting up a request, clean-up can later treat half-built data as valid.
- CVE-2026-109619, RCE via stale global state (GHSA-6wmf-4rv5-p3rp). After a fatal error, some internal state can still point at freed memory. The next request on the same worker may use it.
Both are rated 9.2, critical, on CVSS version 4.0. No login and no user click are needed. The score also marks the attack as high complexity with extra conditions, so it does not work the same way everywhere.
Who is at risk from the October 2026 PHP security release?
Every PHP installation that serves web requests is at risk until it is updated, says php.net. The advisory names PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed, and rates the risk highest for mod_php.
The detail differs by SAPI, because each one starts and cleans up requests in its own way. The table sums up what the two GitHub advisories and the PHP Foundation say.
| How PHP runs | CVE-2026-109618 (out of memory) | CVE-2026-109619 (stale state) |
|---|---|---|
| Apache with mod_php | Confirmed. Unauthenticated RCE shown over HTTP with default limits, before any app code runs. | RCE confirmed through session handling, and on PHP 8.5 through an error setting explained below. |
| PHP-FPM (the store's setup) | Susceptible by source analysis, says the advisory. The Foundation says RCE was confirmed through application code paths. | RCE through session handling validated. |
| FrankenPHP and LiteSpeed | Likely susceptible, not yet validated. | Listed as at risk by php.net. |
| php-cgi | Listed as at risk by php.net. | Listed as at risk by php.net. |
| Command-line scripts and cron jobs | Not in php.net's list, which covers PHP serving web requests. | Not in the list either. Update anyway, because the fix ships in the same packages. |
Wiz adds two claims worth knowing. It says nearly every PHP version back to PHP 5.4 has at least one of the bugs. It also says even a plain Hello World app can be vulnerable, because the bugs sit below the app.
So the store is at risk. Its cart code does not matter, and PHP-FPM is on the validated list for the session path.
How does one PHP worker serve many requests?
A worker is one process that handles requests one after another, keeping some internal state between them. Not starting PHP afresh for each request saves time, and that shared process is the stage both bugs play on.
Each request goes through the same steps. PHP sets up internal state, then parses the request: the body, cookies, file uploads and session data. Only then does your code run. Afterwards PHP cleans up and resets state for the next request.
Fatal errors take a shortcut. When a request hits memory_limit or a timeout, PHP does not return step by step. It jumps straight to shutdown, a move PHP's source code calls a bailout.
One request inside a PHP worker
Drag to exploreNotice the Yes arrow: a fatal error skips straight to shutdown. Bug one lives on that jump. Bug two lives where the reset for the next request gets skipped.
Diagram connections:
- Incoming HTTP request to Request start-up (Request arrives)
- Request start-up to Parse body and cookies (Set-up done)
- Parse body and cookies to Fatal error or OOM? (Any emergency?)
- Fatal error or OOM? to Your PHP code runs (No)
- Fatal error or OOM? to Shutdown and clean-up (Yes: jump straight here)
- Your PHP code runs to Shutdown and clean-up (Code finished)
- Shutdown and clean-up to Reset for next request (Worker reused)
- Incoming HTTP request: Browser or attacker → PHP worker
- Request start-up: PHP sets up its internal state
- Parse body and cookies: Uploads and sessions, before any of your code
- Fatal error or OOM?: Out of memory (OOM) or a timeout, at any step
- Your PHP code runs: The cart, WordPress, Laravel and so on
- Shutdown and clean-up: Frees the request's memory
- Reset for next request: Same worker, same memory, new request
Think of a hotel room that guests use in turn. Check-in fills a form, the stay is your code, check-out tidies up, and housekeeping resets the room for the next guest. A bailout is a fire alarm: everyone leaves at once, wherever they were.
Bug one is check-out reading a half-filled check-in form as if it were complete. Bug two is housekeeping being skipped, so the next guest finds the last guest's key to a room that was already given to someone else.
Where the picture breaks: in a hotel, nobody chooses what ends up in the freed room. In PHP, an attacker can shape what lands in freed memory, and that is what turns a crash into code execution.
How does running out of memory corrupt a PHP request (CVE-2026-109618)?
The out-of-memory stop can fire while PHP is still building request data, so clean-up later reads data that was never finished. GHSA-w8hw-w9fx-j79w says request shutdown may then read or branch on that invalid state.
In the source before the fix, the memory manager raises the familiar fatal error and then calls zend_bailout(). That function ends with longjmp, a C jump that skips every remaining line in the functions it leaves.
You can watch the shortcut safely on any PHP install. This one-liner sets memory_limit to 2M (2 MiB), registers a shutdown function, then tries to build a 4 MiB string.
php -d memory_limit=2M -r 'register_shutdown_function(function () { echo "shutdown code still runs\n"; }); echo "step 1: start\n"; $big = str_repeat("x", 4 * 1024 * 1024); echo "step 2: never printed\n";'Pause and predict: will step 2 print, and will the shutdown function run?
On PHP 8.3.6 we saw step 1, then a fatal error reading Allowed memory size of 2097152 bytes exhausted, then shutdown code still runs. 2097152 bytes is exactly 2 MiB. Step 2 never printed, and the exit code was 255.
What just happened? The line after the failed memory request never ran, yet the shutdown code did. That is harmless in your script. The bug appears when the same jump lands in the middle of PHP's own set-up code, during request parsing.
The advisory lists ways a request can run out of memory while staying inside the limits PHP appears to set. They include a missing or unusable content length, unlimited headers in one part of an upload, and empty file fields combined with nested field names. That is why size limits feature in the stopgaps below.
How can one request poison the next one (CVE-2026-109619)?
A fatal error can skip the reset of some global state, so the next request on the same worker starts with pointers into freed memory. GHSA-6wmf-4rv5-p3rp calls these a producer request and a consumer request.
The producer hits a fatal error, such as out of memory or a timeout, at the wrong moment. The consumer reaches the same worker and touches that state before normal set-up resets it.
The advisory names confirmed consumers. One is session upload progress, a feature that is on by default and tracks file uploads in the user's session. Another is a configuration warning raised before the request is set up.
There is a second kind of producer too. A non-fatal warning during shutdown can refill error state after PHP has already cleared it.
Wiz's write-up groups the bugs differently and lists its upload-progress bug under CVE-2026-109618. This guide follows the official advisories, where session upload progress is a path into CVE-2026-109619.
Two requests, one worker
Drag to exploreNeither request alone is enough. The danger is the stale pointer that survives between them on the same worker.
Diagram connections:
- Request A (producer) to PHP worker (1. Fatal error mid-request)
- PHP worker to Freed memory (2. Reset skipped, pointer kept)
- Request B (consumer) to Early start-up step (3. Next request starts)
- Early start-up step to Freed memory (4. Uses the stale pointer)
- Request A (producer): Attacker → worker: causes a fatal error
- PHP worker: Frees A's memory, but a global still points there
- Freed memory: May now hold data an attacker arranged
- Request B (consumer): Attacker → same worker, moments later
- Early start-up step: Session upload progress or an INI warning
The strongest confirmed case is PHP 8.5 with fatal_error_backtraces on. That setting, new in PHP 8.5 and on by default, adds a backtrace (the list of function calls) to fatal errors. Hence the advisory's 8.5-only stopgap below.
How do you check whether your PHP is patched?
Check the PHP your web server actually runs, then check the package, because Linux distributions often patch PHP without changing its version number.
This is the trap on the store's Ubuntu 24.04 server. Ubuntu shipped the fix as package version 8.3.6-0ubuntu0.24.04.13, yet php -v still says PHP 8.3.6. Upstream, the fixed 8.3 release is 8.3.36.
Find the PHP that serves the site
Command-line php and web PHP can differ, and on Ubuntu they read different php.ini files. Use php-fpm8.3 -v for PHP-FPM; for Apache, the package is libapache2-mod-php8.3.
Read the version line
If you build from upstream source or use php.net binaries, you need 8.5.12, 8.4.27, 8.3.36 or 8.2.35 or later on your branch.
Check the package for the CVE ids
On Debian or Ubuntu, list the package version with dpkg, then search its changelog for CVE-2026-109618. A count above zero means the fix is in.
Restart and check again
Restart PHP-FPM or Apache so no worker still runs old code, then repeat the checks.
php -v
php-fpm8.3 -v
dpkg -l | grep -E "php8.3-(cli|fpm|common)|libapache2-mod-php"
zgrep -c CVE-2026-109618 /usr/share/doc/php8.3-cli/changelog.Debian.gzOn our patched Ubuntu 24.04 test server, php-fpm8.3 -v printed PHP 8.3.6 (fpm-fcgi), built on 19 October 2026. dpkg showed 8.3.6-0ubuntu0.24.04.13, and zgrep printed 2. Before the update, the package ended in .12 and the build date was 7 October.
On other systems, look up your vendor's security notice for the two CVE ids instead of trusting the version number alone.
How do you patch PHP on common setups?
Install your vendor's patched package or the fixed upstream release, then restart every PHP process. The table covers the setups most readers run.
| Your setup | What to do | Watch out for |
|---|---|---|
| Ubuntu or Debian packages | Run your normal security update, for example apt update and then apt upgrade. | The version number may not change. Check the changelog for the CVE ids. |
| Other Linux distributions | Install your vendor's updated packages. php.net says vendors received the patch in advance. | The volunteer-run rpms.remirepo.net had no update on 21 October. php.net suggests distribution packages or a source build. |
| Docker images | Rebuild on a base image with fixed PHP and redeploy every container. | A running container keeps its old PHP until it is replaced. |
| Windows | Download the fixed builds from php.net's Windows downloads page. | Restart the web server or FastCGI host afterwards. |
| Shared hosting or managed WordPress | Ask your host whether its PHP has the October 2026 fix, quoting both CVE ids. | Updating WordPress or plugins does not fix a PHP runtime bug. |
| PHP 8.1, 8.0 or 7.4 | No official release. Use an LTS vendor's backport, the php.net source backport branches, or upgrade. | These branches get no regular fixes, so plan the upgrade anyway. |
| PHP 8.2 | Update to 8.2.35. | PHP 8.2 reaches end of life on 31 December 2026. |
The store's developer runs the Ubuntu update. The package tries to restart PHP-FPM by itself. On our locked-down test machine that restart was blocked, a reminder to confirm the restart rather than assume it.
What can you do if you cannot update PHP today?
Only updating is a complete fix, and php.net says so plainly. Its stopgaps reduce specific attack paths but do not make an installation safe.
Stopgaps for the out-of-memory bug (CVE-2026-109618)
- Set memory_limit=-1 with php_admin_value in the FPM pool or Apache config, so apps cannot set a limit back. Real allocation failures can still reach the bug.
- Keep post_max_size low: 8M if the app takes no uploads, 32M if it does. Keep upload_max_filesize below post_max_size.
- Limit request size, field count, upload parts, headers and nesting at the web server or proxy. Cover requests with no usable Content-Length too.
- Use a container or cgroup memory limit as the safety net. Do not use RLIMIT_AS or RLIMIT_DATA, which can cause the very allocation failure you are avoiding.
Stopgaps for the stale-state bug (CVE-2026-109619)
- Set session.upload_progress.enabled=0 in php.ini or the pool config, then restart every worker. The advisory suggests keeping it off after patching too, if you do not need it.
- On PHP 8.5, set fatal_error_backtraces=0 and restart every worker.
- Remove invalid or deprecated per-request settings that cause warnings before set-up. Hiding the warnings does not help.
- Fix fatal errors and timeouts that an attacker can trigger. GHSA-6wmf also suggests pm.max_requests=1, one request per worker, if you can afford the speed cost.
Where you set these matters. We tested two lines in a PHP-FPM 8.3.6 pool config:
php_admin_value[session.upload_progress.enabled] = 0
php_admin_value[memory_limit] = -1A test script in that pool read upload progress as 0 and memory_limit as -1. Its own ini_set call to lower memory_limit to 64M returned false and changed nothing. Without the two lines, the same call worked.
Session upload progress cannot be switched off from app code at all. In our test, ini_set() returned false for it. The advisory also warns against .user.ini files, so set it in php.ini, the pool config or the server config.
Turning off the memory limit has a cost: one runaway script can now eat the server's RAM. The advisory's container or cgroup limit is the safety net, ideally set to kill only the affected worker.
A web application firewall (WAF), a filter in front of your site, can buy time. Cloudflare says it deployed rules about nine hours before disclosure, on by default for free plans, and still tells customers to patch.
To spot attempts, Wiz suggests watching for odd multipart requests followed by memory-limit errors and bursts of HTTP 500 errors. It also flags oversized or malformed PHPSESSID cookies, and workers restarting for no clear reason.
How did the PHP team fix the bugs?
One large commit titled Avoid state corruption via bailout fixes both CVEs. On the 8.2 branch it touches 389 files, adding 10,759 lines and removing 988.
Its message lists two changes. It replaces the out-of-memory bailout with a controlled unwind, and it properly resets globals that may be read during start-up.
The advisory explains the new out-of-memory rule. Going over memory_limit now sets a soft error that fires at the next safe point in the engine. Going over a higher hard limit ends the process at once with _exit().
A new setting that is not documented yet
The hard limit comes from a new setting, memory_hard_limit_percent, which can be changed anywhere, like memory_limit. A comment in the 8.2.35 source gives its default as 200% if the memory limit is under 1G, and a fixed 1G buffer otherwise.
Values must be 100 or more, or -1 for the default. It is undocumented as of 21 October 2026: we found it in the source and in php -i on Ubuntu's patched build, not in the PHP manual. Pin your version before you rely on it.
What the fix does not change is the message you see. On the patched Ubuntu build, our 2M one-liner printed the same fatal error, and the shutdown function still ran. The change is inside the engine, so never judge a server by its error output.
How was PHP2Shell found, and why did the release move a day early?
Several teams found the same root cause, and leaks forced the release forward by a day. The PHP Foundation's own post sets out the timeline.
- 2024: during an external audit, a PHP core developer spotted the first signs of the out-of-memory problem, but no exploit path.
- 11 August 2026: the security firm Calif and a separate AI-assisted research group reported the same root cause on the same day.
- 24 August 2026: Wiz reported further bugs, found with what it describes as AI-assisted research.
- 20 October 2026: the CVE ids leaked with a wrong CVSS score of 10.0. php.net pre-announced the release and moved it from 22 to 21 October.
- 21 October 2026, 14:00 UTC: the fixed releases and both advisories went public.
The Foundation is frank about the gap between August and October. It says the team struggled to choose between competing fixes, and calls the delay a structural failure. It is moving the security team to majority decisions.
That also explains the Hacker News titles. Posts that said CVSS 10.0 were repeating the leak. The official score is 9.2.
How do you explain PHP2Shell in a system design interview?
Use it as a case study in shared state between requests, then show how you would roll out a runtime fix safely.
Name the class of bug
Say it is a use-after-free in a long-running worker: an error path skips clean-up, and the next request uses freed memory.
Explain why the app cannot fix it
The bug can fire during request parsing, before any app code runs, so patching the runtime is the only full fix.
Rank the stopgaps
WAF rules and config changes cut known paths. Say clearly that they are incomplete, and why.
Plan the rollout
List every PHP runtime, including containers and hosted sites. Patch, restart workers, and verify by package changelog rather than version string.
Close with the design lesson
For untrusted input, prefer full state resets or process isolation between requests, and weigh the speed cost, as pm.max_requests=1 shows.
Questions people ask
- Do I need to update WordPress, Laravel or my plugins to fix PHP2Shell?
- No, updating your app does not fix it. The bugs are in the PHP runtime, so the fix is PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or your vendor's patched package. Cloudflare's post makes the same point: updating site code or plugins alone does not fix these flaws. Keep your app updated anyway, because the advisory also asks you to fix fatal errors that attackers can trigger.
- Is PHP2Shell rated CVSS 10.0 or 9.2?
- It is 9.2 on CVSS version 4.0, for both CVEs. The 10.0 figure came from a leak on 20 October 2026, together with the CVE ids and a wrong release time, according to the PHP Foundation. php.net's own pre-announcement that day already said 9.2, and both GitHub advisories list 9.2 as critical.
- Why does php -v still show an old version after I updated?
- Your Linux distribution probably backported the fix without changing PHP's version number. On Ubuntu 24.04, the patched package as of 21 October 2026 is 8.3.6-0ubuntu0.24.04.13, and php -v still prints 8.3.6. Check the package version and search its changelog for CVE-2026-109618. Builds from upstream source or php.net binaries show the new number, such as 8.3.36.
- Does a web application firewall protect me from PHP2Shell?
- It lowers the risk but does not remove it. Cloudflare deployed managed rules about nine hours before disclosure, and it still says customers should patch. It also notes that attackers were changing their payloads to slip past protections. Treat a firewall as time to patch, not as a replacement for patching.
- I run PHP 8.1 or older. What should I do?
- Upgrade if you can, because the PHP project no longer ships releases for 8.1 and older. Given the severity, the PHP security team published minimal source backports for 7.4, 8.0 and 8.1 as branches in the php-src repository. Some distributions and long-term support vendors also ship backported fixes, so ask yours. PHP 8.2 itself reaches end of life on 31 December 2026.
- Do I have to restart anything after updating PHP?
- Yes, restart PHP-FPM, Apache or whatever runs PHP. A running process keeps the old code it loaded when it started, so an updated package on disk is not enough. The advisory also says to restart every worker after changing settings such as session.upload_progress.enabled. Then run your version and changelog checks again.
- Was PHP2Shell found by AI?
- Partly, by people using AI tools. Wiz says its researchers used AI-assisted research to find and test the bugs, and the PHP Foundation says another AI-assisted group reported the same root cause. The Foundation also traces the first signs to a 2024 audit, and says a person makes every call and works on every patch.
Sources and further reading
- php.net: Security advisory, critical remote code execution vulnerabilities, update now (21 Oct 2026)
- php.net: Upcoming critical PHP security release, CVSS 9.2 (20 Oct 2026)
- GHSA-w8hw-w9fx-j79w: RCE via OOM corruption (CVE-2026-109618)
- GHSA-6wmf-4rv5-p3rp: RCE via stale global state (CVE-2026-109619)
- php.net ChangeLog: PHP 8.5.12
- php-src fix commit: Avoid state corruption via bailout
- php-src main/main.c at tag php-8.2.35 (memory_hard_limit_percent)
- PHP manual: error handling settings (fatal_error_backtraces)
- The PHP Foundation: Today's PHP security release, and how we're strengthening PHP security
- Wiz Research: PHP2Shell
- Cloudflare: Cloudflare deploys protections against php2shell vulnerability