October 23, 2026
PHP2Shell Explained: How One Out-of-Memory Error Turned Core PHP Into a Remote Shell
Two critical flaws CVE-2026โ109618 , CVE-2026โ109619 in the PHP engine itself. No login needed. PHP2Shell attack chain

By Uday Sharma
13 min read
If you run a website, there is a good chance PHP is somewhere underneath it. WordPress, Drupal, Joomla and MediaWiki (the software behind Wikipedia) are all PHP applications.
On October 21, 2026, the PHP project shipped emergency security releases for two critical bugs that researchers nicknamed PHP2Shell. These bugs are not in a plugin. They are not in WordPress. They are in PHP's core engine, which means a site with perfectly clean code can still be exposed.
This article walks through the whole story end to end: what PHP2Shell is, why it happens, why "no login required" is true, who is affected, and exactly what to do about it.
No security background needed. If you can follow a story about a restaurant kitchen, you can follow this.
Table of Contents
- TL;DR
- What is PHP2Shell?
- Jargon decoder
- PHP 101: how a request is handled
- The harmless feature at the center: session upload progress
- The bug: a bailout at the wrong moment
- Why "unauthenticated"? (and is it about storage?)
- The two CVEs and four bug types
- Who is affected?
- Timeline: from discovery to patch
- Is it being exploited?
- The AI angle
- How to protect yourself
- How PHP fixed it
- Lessons for developers
- FAQ
- Production checklist
- References
1. TL;DR
- What: Two critical vulnerabilities in the PHP engine, CVE-2026โ109618 and CVE-2026โ109619, nicknamed PHP2Shell.
- Severity: Both are rated 9.2 on CVSS v4.0 (Critical).
- Impact: Unauthenticated remote code execution (RCE) and Heartbleed-style memory disclosure.
- Root cause: When PHP hits a fatal error such as running out of memory, it can skip cleanup and leave stale pointers behind. A later request on the same worker can then reuse that stale state.
- Why no login is needed: The bug fires while PHP is still reading the request, before your application code (and your login check) runs.
- Fix: Upgrade to PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35. PHP says there are no complete mitigations, and that updating is the only safe choice.
2. What is PHP2Shell?
"PHP2Shell" is the public nickname for a pair of vulnerabilities in the PHP interpreter. The name says it all: PHP to shell, meaning an attacker goes from sending a web request to running commands on your server.
Researchers from Wiz, Calif and OpenAI jointly disclosed the issues. One reported detail worth remembering: Wiz says its team showed pre-authentication RCE against fresh default Docker images of WordPress, Drupal and Joomla, and that even a simple "Hello World" PHP app on an affected version can be vulnerable.
Wiz also claims that PHP powers about 70% of all websites. Treat that figure as Wiz's claim, but the direction is clear: this is a very large attack surface.
3. Jargon decoder
Before the story, a quick glossary. You will see these words a lot.
4. PHP 101: how a request is handled
Here is the one idea you need to understand the whole bug.
A PHP server does not start a brand-new process for every visitor. It keeps a pool of workers alive and hands them requests one after another. Each request gets its own chunk of memory, and when the request ends, that chunk is wiped.
Think of a hotel with a small number of rooms:
- Each guest (request) gets a room (memory).
- When the guest leaves, housekeeping (cleanup) resets the room.
- The next guest gets the same room.
That works perfectly as long as housekeeping always runs.
PHP2Shell is what happens when a guest leaves in an emergency, housekeeping is skipped, and an old room key is still floating around. The next guest in that room can end up with something they should never have.
5. The harmless feature at the center: session upload progress
PHP has a built-in feature called session upload progress (session.upload_progress).
When a browser uploads a big file, PHP can record how far the upload has got (name, size, bytes received) into the user's session data, so a second request can read that and draw a progress bar.
Two details matter:
- It has existed since PHP 5.4 (2012) and is enabled by default.
- The progress data is written while PHP is still reading the request body, which is before any application code runs.
The feature itself is not the bug. It simply gives a remote request a way to make PHP do real work, very early, with data the visitor partly controls. (JFrog notes the visitor influences the session ID, part of the session key, and the uploaded filename.)
6. The bug: a bailout at the wrong moment
Now put the pieces together.
PHP enforces a memory_limit. When a request goes over it, PHP can't just carry on. It does a bailout: it uses longjmp() to jump straight back to a safe recovery point and abandons whatever it was doing.
The problem: "abandon what you were doing" also means "skip the cleanup you were about to do."
Here is the story in the kitchen analogy:
A chef is halfway through preparing an order when the fire alarm rings. He runs out and leaves everything mid-step: knives out, a label still pointing at a shelf that is about to be emptied. The kitchen is reset for the next shift, but one sticky note still says "ingredients are on shelf 4." The next chef trusts the note.
In PHP terms:
- Request 1 (the attacker's first request) makes PHP start decoding session data and builds internal bookkeeping. A global variable keeps a pointer to it.
- The request runs out of memory at exactly the wrong moment.
- PHP bails out with
longjmp(). The request's memory is freed, but the global pointer is not cleared. - The worker process survives and is reused.
- Request 2 lands on the same worker. The attacker has arranged for the freed memory to be refilled with crafted bytes.
- PHP trusts the stale pointer and treats those bytes as valid internal objects. That is a use-after-free.
- Depending on the variant, the result is memory corruption leading to code execution, or leaked memory sent back to the attacker.
A tiny pseudocode picture (this is a teaching sketch, not real PHP source):
// Teaching sketch only
global_ptr = allocate_bookkeeping(); // request-owned memory
do_risky_work(); // <-- out-of-memory happens here
// longjmp() jumps away...
cleanup(global_ptr); // <-- ...so this line never runs
global_ptr = NULL; // <-- and the pointer stays "live"
// request ends, heap is freed, worker is reused
// next request: global_ptr still points into freed memory// Teaching sketch only
global_ptr = allocate_bookkeeping(); // request-owned memory
do_risky_work(); // <-- out-of-memory happens here
// longjmp() jumps away...
cleanup(global_ptr); // <-- ...so this line never runs
global_ptr = NULL; // <-- and the pointer stays "live"
// request ends, heap is freed, worker is reused
// next request: global_ptr still points into freed memoryWhy is this hard to spot?_ Each line looks fine on its own. The danger only shows up when you combine_ error handling, global state and long-lived workers.
One more important nuance from researchers: the size of the HTTP body is not the whole story. Calif notes that small inputs with nested names can balloon into much larger internal structures, so "block big uploads" is not a complete defense.
7. Why "unauthenticated"? (and is it about storage?)
This was a great question, so let's answer it carefully.
Short answer: your intuition about storage is partly right, but the better word is memory.
What "unauthenticated" really means here
Normally, "bypassing login" means tricking the login page. That is not what happens here. There is no login to trick.
Look at the request pipeline:
- Client sends a request.
- Web server passes it to PHP.
- PHP engine starts up and parses the request body. โ the bug fires here
- Your app (WordPress, Laravel, your custom login) starts running. โ never reached
The login check, if ($password == $correct), lives in step 4. The exploit happens in step 3. The gatekeeper is not defeated, it is never consulted.
This also explains Sophos's warning that in-process protections such as RASP or application-embedded WAF agents may not stop exploitation on Apache mod_php, because the trigger can happen before any application code runs.
Where "storage" fits in
Two kinds of storage are involved:
So: the session store carries the attacker's data in, and bad memory cleanup is the actual flaw.
Small corrections to common explanations
- It is not that PHP "executes hidden commands" because it reads half-built data. Instead, PHP follows a stale pointer into memory the attacker has refilled, and treats attacker bytes as real internal objects. That lets the attacker steer what PHP does next.
- The two CVEs are not "one is out-of-memory and the other is use-after-free." Both come from the same family of mistake: stale state left behind after a fatal error. They differ in which piece of state is left behind (next section).
8. The two CVEs and four bug types
Wiz grouped four exploitable bugs under the two CVEs. All four share the same pattern: a fatal or out-of-memory error leaves something stale behind.
The memory-leak variant (Heartbleed-style)
This one deserves its own mention because it can hurt even if nobody gets a shell. A later request can reach cleanup code that trusts a stale session ID pointer, and PHP then URL-encodes the reclaimed memory into a PHPSESSID response cookie. Wiz says this works even when the application never calls session_start().
Why that is scary: worker memory may contain passwords, tokens, API keys and certificates from other requests.
The OPcache variant (cross-worker)
OPcache stores compiled PHP structures in memory shared by all workers. If memory runs out between two bookkeeping steps, a slot can end up half-published. Another worker later reuses that slot for a different kind of object, and PHP ends up reading it as the wrong type. That is a type confusion, and it can corrupt shared state.
9. Who is affected?
A note on reality: exploitation is not equally easy everywhere. Setup details such as the server API, worker settings and memory configuration affect reliability, and reports differ on how complete FPM exploits are. Do not treat "it is hard on my setup" as a reason to wait.
10. Timeline: from discovery to patch
Defenders had a rough 24 hours: a scary warning, no technical details, no patch yet, and signs that attackers already had a head start.
11. Is it being exploited?
Yes. According to several security vendors:
- watchTowr says its honeypot network saw in-the-wild exploitation of both CVEs on October 21.
- Hadrian is quoted by SecurityWeek (via Corgea's write-up) as seeing exploitation "at scale," mostly through the session upload-progress path.
- Recorded Future reports many public proof-of-concept repositories on GitHub, with mixed effectiveness (some rely on techniques later invalidated).
Warning: fake PoCs
JFrog found that some GitHub "PoC" repos were pranks that did not exploit anything, and at least one changed the victim's wallpaper. The lesson is simple: do not run random exploit code from the internet on your own machine or your servers. If you need to test, use an isolated lab and trusted sources.
12. The AI angle
This might be the most interesting part of the story for people who follow AI.
Wiz says the vulnerable code had existed for more than a decade, but turning these flaws into working exploits required chaining several bugs and adapting to memory layouts, work that was at the edge of what human researchers could practically do. According to Wiz, frontier AI models lowered that barrier:
- Wiz says it used its Wiz Atlas tooling, Gemini 4 Argon and GPT-5.6 Sol throughout the research.
- It credits Gemini 4 Argon with a major contribution to finding the bug, exploit development, detection and patch validation.
- It also says a single researcher completed in about a week what previously might have taken a world-class team months.
- JFrog notes the disclosure was part of OpenAI's "Patch the planet" initiative.
Wiz's conclusion is that complexity may stop being a meaningful barrier, which changes the economics for both defenders and attackers. These are Wiz's claims and viewpoint, but the takeaway for engineers is clear: assume the gap between "disclosed" and "exploited" is shrinking.
13. How to protect yourself
Step 1: Check your PHP version
php -v
# Inside Docker:
docker exec <container> php -vphp -v
# Inside Docker:
docker exec <container> php -vNote that the web server's PHP can differ from the CLI PHP on the same machine. Check the one that actually serves your site (for example via a phpinfo page or your hosting panel, and remove it afterwards).
Step 2: Upgrade (the real fix)
Upgrade to the newest release in your branch:
PHP's own release notes are blunt: there are no complete mitigations, and updating is the only safe choice. Remember to restart PHP workers (FPM, Apache, FrankenPHP) after upgrading, and rebuild and redeploy container images.
Step 3: If you truly cannot patch yet, use interim mitigations
These are partial. Treat them as a bridge, not a fix.
Example php.ini lines (the first two are the ones most vendors point to):
; Interim mitigation only - upgrading is the real fix
session.upload_progress.enabled = 0
; PHP 8.5 only:
fatal_error_backtraces = 0; Interim mitigation only - upgrading is the real fix
session.upload_progress.enabled = 0
; PHP 8.5 only:
fatal_error_backtraces = 0Two practical gotchas:
- Restart every worker after changing settings, or the old values stay active.
- A third-party mitigation repo notes that
ini_set()in your script does not help, because this setting is applied at the per-directory level and the request body is parsed before your code runs. Change it inphp.ini, pool config, or server config.
Step 4: Hunt for signs of trouble
Wiz suggests watching for:
- Unusual multipart requests followed by memory-limit errors or repeated HTTP 500 responses
- Unexpectedly large or malformed
PHPSESSIDresponse cookies - Unexplained PHP worker churn (workers crashing and restarting)
Application logs may show little, because the bug lives below the application layer.
Step 5: Assume secrets might be exposed (if you were reachable)
Because of the memory-disclosure variant, if an unpatched server was internet-facing during the exposure window, consider rotating high-value secrets (API keys, tokens, database credentials) and reviewing for unexpected files or processes. This is a prudent response, not a confirmed breach.
14. How PHP fixed it
According to JFrog's analysis of the shipped patch, the fix works in three layers:
- Out-of-memory no longer aborts mid-operation. OOM becomes a soft error at the next VM safe point (or a hard exit past a higher threshold), so cleanup always runs and the pointer is cleared before the memory is freed.
- Defense in depth. Even if a request dies another way, long-lived state (backtrace, OPcache, session, error state) is reset at shutdown, so nothing stale survives to the next request.
- No user code during upload-progress decode. Objects decoded there stay as incomplete classes, with no autoload and no destructors, closing the object-injection delivery route.
Wiz adds that the patch fixes the issue at its core and covers other variants it believes exist under the same two CVEs.
15. Lessons for developers
- Your framework is not your whole attack surface. The runtime under it is too. Track PHP, Node, Python and JVM versions like you track dependencies.
- Pin and know your versions. If you can't answer "what PHP version is production running?" in under a minute, fix that today.
- Global state plus error handling is dangerous. Any cleanup that can be skipped by a jump, crash or exception is a future bug.
- Long-lived workers amplify mistakes. Reuse is great for speed, but it lets one request's leftovers leak into the next.
- Don't ship end-of-life runtimes. PHP 8.1 and earlier stay vulnerable, and the clean path is to upgrade.
- Never run random PoCs. Fake exploit repos are a real pattern after every big disclosure.
- Plan for faster exploitation. If AI shortens exploit development, patch windows shrink. Automate upgrades.
16. FAQ
Is this the same as a WordPress plugin vulnerability?
No. It is in PHP itself, so every app on an affected PHP is exposed, regardless of how well it is coded.
Do I need to be logged in to be attacked?
No. That is what "unauthenticated" means here.
I use PHP-FPM, not Apache. Am I safe?
Not safe. PHP lists FPM among affected setups. Apache mod_php is the highest-risk deployment according to PHP.
Can a WAF or a security plugin save me?
It may help as an extra layer, but Sophos warns in-process protections may not stop exploitation on mod_php. Patch first.
My PHP version isn't listed. What now?
If it is 8.1 or older, it is end-of-life and still vulnerable. Move to a supported branch.
Does disabling upload progress fix everything?
No. It removes the trigger for several techniques but not the OPcache or backtrace variants.
Was my site hacked?
This article can't tell you that. Check logs for the signals above and follow your incident process if you see them.
17. Production checklist
- dentify every PHP version running (web servers, containers, CI images, hosting panels)
- Upgrade to 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 (or move off EOL branches)
- Rebuild and redeploy container images, then restart all workers
- If delayed:
session.upload_progress.enabled=0,fatal_error_backtraces=0(8.5), edge rules for multipart - Turn on alerts for memory-limit errors, HTTP 500 spikes, and odd
PHPSESSIDcookies - Review exposure window; rotate high-value secrets if internet-facing and unpatched
- Subscribe to PHP security announcements and your host's advisories
- Never test with unknown GitHub PoCs
18. References
Official / primary
- PHP 8.5.12 release notes (GitHub): https://github.com/php/php-src/releases/tag/php-8.5.12
- PHP 8.4.27 release notes (GitHub): https://github.com/php/php-src/releases/tag/php-8.4.27
- PHP security advisories: GHSA-w8hw-w9fx-j79w and GHSA-6wmf-4rv5-p3rp (linked from the JFrog post)
- The PHP Foundation blog post on the release (linked from the Wiz post): https://thephp.foundation/blog/2026/10/21/todays-php-security-release-and-how-were-strengthening-php-security/
Research and vendor write-ups
- Wiz Research: https://www.wiz.io/blog/php2shell-php-rce
- JFrog Security Research: https://research.jfrog.com/post/php2shell-all-you-need-to-know-about-cve-2026-109618-and-cve-2026-109619/
- Sophos advisory: https://www.sophos.com/en-us/security-advisories/sophos-sa-20261021-php2shell
- Sansec: https://sansec.io/research/php2shell
- watchTowr: https://watchtowr.com/intelligence/php-remote-code-execution-php2shell/
- Imperva: https://www.imperva.com/blog/php2shell-vulnerability-updates/
- Recorded Future: https://www.recordedfuture.com/blog/php2shell-vulnerability-exploitation
- Brandefense: https://brandefense.io/blog/php2shell-php-rce-cve-2026-109618-109619/
- Calif: https://calif.io/php2shell/
- Cloudflare WAF guidance: http://blog.cloudflare.com/php-vulnerabilities-waf-protection/
Found this useful?
X: x.com/aiwithuday ยท GitHub: github.com/UdaySharmaGitHub ยท LinkedIn: linkedin.com/in/uday-sharma-602b33267 ยท Medium: medium.com/@neuraldev