Pune, IN18°31'13"N 73°51'24"E--:--:-- IST

All posts

PHP2Shell: How Running Out of Memory Became Remote Code Execution

  • php
  • vulnerabilities
  • incident-response

Level: Intermediate to advanced (every hard concept is explained as we go)

PHP2Shell cover: CVE-2026-109618 and CVE-2026-109619, CVSS 9.2, no login needed, fixed in PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35


Most web vulnerabilities live in an application: a WordPress plugin, a login form, a file upload page. You fix them by updating that one app. PHP2Shell is different. The bugs are in the PHP engine, the program that runs every PHP application. PHP powers roughly 70% of websites whose server language is known, so the blast radius is WordPress, Drupal, Joomla, Magento, Laravel apps, hosting control panels, firewalls with PHP web interfaces, and the “Hello World” page you made in 2015 and forgot about.

The researchers who found it demonstrated unauthenticated remote code execution against stock WordPress, Drupal and Joomla, and a way to read chunks of server memory from any PHP app. Then the details leaked online hours before the patch, and attacks started before the fix existed.

This post covers all of it: what broke, why, how attackers chain it into a shell (explained conceptually, no exploit code), who is exposed, how to detect it, and exactly what to do.

Scope and ethics. Everything here comes from the PHP project’s own advisories, the discoverers’ write-up, and national CERT and vendor reports, all linked at the end. I explain mechanisms so defenders understand them. There is no working exploit here. Only test systems you own or are authorized to test.

The short version

  • What: Two critical memory-safety flaws in the PHP engine, nicknamed PHP2Shell by Wiz Research, who found them.
    • CVE-2026-109618: “RCE via out-of-memory (OOM) corruption.”
    • CVE-2026-109619: “RCE via stale global state.”
  • Severity: Both CVSS v4.0 9.2 (Critical). No login and no user interaction needed.
  • Affected: Essentially every PHP version from 5.4 (2012) onward, in normal web setups. Highest risk: Apache with mod_php. PHP-FPM, php-cgi, FrankenPHP and LiteSpeed are also at risk.
  • Fixed in: PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 (21 October 2026). Minimal source backports exist for 7.4, 8.0 and 8.1. Nothing for 7.3 and older.
  • Status (23 Oct): Exploitation attempts are massive: Cloudflare counts millions, and Amazon saw a probe four days before disclosure. No successful break-in has been publicly confirmed yet, and neither CVE is in CISA’s Known Exploited Vulnerabilities list so far. CISA and the NSA have told organizations to patch immediately.
  • What you do: Update PHP (or your distro’s PHP package), restart every PHP worker, then check for signs of compromise going back to at least 17 October.

Update, 23 October 2026: what’s new

The story moved fast in the 48 hours after the patch. Here’s what changed:

  • Attacks started before anyone knew. Amazon’s threat intelligence team says its honeypot network (MadPot) caught a PHP2Shell exploitation attempt on 17 October, four days before disclosure, from an anonymized IP address. Amazon calls it largely a one-off probe and can’t explain how the attacker knew about the bugs while they were still secret. Amazon now advises investigating activity back to 17 October, not 20 October.
  • The numbers exploded after disclosure. Cloudflare CEO Matthew Prince reported thousands of attempts in the 10 hours before the announcement, tens of thousands within 45 minutes after, hundreds of thousands within two hours, and millions within about 12 hours. Cloudflare says attackers are tweaking their payloads to get around its WAF rules, using blocked requests as feedback.
  • Attempts are not the same as break-ins. As of 22 October, threat intel firm KELA and several news outlets report no publicly confirmed successful compromise. KELA says criminal forums are loud but mostly asking for working tools, and at least one “0day for sale” ad looks like a scam. Its summary: “loud is not the same as capable.” Don’t relax, though; capability is catching up.
  • CISA and the NSA weighed in (21 October), urging immediate patching, extra logging, and taking unpatchable or end-of-life PHP systems offline. As of 22 October, neither CVE was in CISA’s KEV catalog.
  • PHP admits one bug was spotted in 2024. In a public statement, the PHP Foundation said a 2024 external security audit first surfaced signs of the CVE-2026-109618 problem, but the team couldn’t agree on a fix. Every option risked breaking compatibility, adding denial-of-service risk, or forcing changes on many extensions, and the work kept stalling. The Foundation has announced changes so stuck critical bugs reach its leadership much sooner.
  • How the embargo fell apart. Per the PHP Foundation, the researchers at Calif reported the bugs on 11 August (Wiz’s own timeline gives 24 August for its report). Rumors hit social media two days before release. A researcher accidentally published a short video of a working exploit, which spread even after it was deleted. Then on release morning, Chainguard shipped patched container images about six hours early, which the Foundation says let attackers study the fix before everyone else could install it. Chainguard says attacks had already started and it made a judgment call.

The advice hasn’t changed: update PHP, restart workers, and hunt for compromise back to 17 October.

The whole bug in one analogy

Picture a hotel room (a PHP worker) that hosts one guest after another (each guest is a web request). Between guests, housekeeping clears the room and wipes the whiteboard where the last guest wrote notes like “my bag is in locker 12.”

Now the fire alarm goes off mid-stay (PHP runs out of memory). Everyone evacuates instantly and housekeeping is skipped. Locker 12 gets handed to someone else, but the note on the whiteboard still says “bag in locker 12.” The next guest walks in, reads the note, and trusts it.

If an attacker can (1) pull the fire alarm on purpose, (2) be the one who gets locker 12 next, and (3) arrange for the next guest to act on that note, they control what happens in the room. That is PHP2Shell. The rest of this post is the technical version of those three steps.

Key terms (keep this handy)

Term Plain meaning
Worker A long-running process that handles many web requests, one after another
Pointer A memory address: “the data you want is over there”
Use-after-free (UAF) Following a pointer to memory that was already given back and possibly reused
OOM Out of memory: the request hit PHP’s memory_limit
Bailout / longjmp() PHP’s emergency exit that jumps out of whatever it was doing, skipping normal cleanup
SAPI How PHP plugs into the web server: mod_php, PHP-FPM, php-cgi, FrankenPHP, LiteSpeed
Multipart The multipart/form-data format browsers use for forms with file uploads
ASLR Address randomization: the OS shuffles where things sit in memory so attackers can’t guess

Background: how PHP actually runs your code

To understand these bugs you need three ideas. None of them are hard.

1. Workers are reused, requests are not

When a request hits a PHP site, it’s handled by a worker process. With Apache mod_php, the worker is an Apache process with PHP built in. With PHP-FPM, it’s a pool of PHP processes that Nginx or Apache hands requests to.

The key detail: a worker doesn’t die after one request. It handles request 1, cleans up, then handles request 2, request 3, and so on, sometimes thousands of times. Starting a fresh process for every request would be too slow.

So inside each worker there are two kinds of memory:

Request memory Global (process) state
Lives for One request The life of the worker
Holds Your variables, parsed form data, session data, error info Pointers and bookkeeping PHP keeps between requests
Cleaned up Thrown away in bulk at the end of every request Supposed to be reset at the end of every request

That last cell is where CVE-2026-109619 lives.

2. A pointer is an address, and a stale pointer is a lie

A pointer is a note that says “the thing you want is at memory address X.” If the memory at X is freed and later reused for something else, the note is now stale. It still says X, but X holds different data. Using it is called a use-after-free (UAF), one of the most exploited bug classes in existence. If an attacker can control what gets put at X next, they control what PHP thinks it’s reading.

3. PHP’s emergency exit skips the cleanup

PHP has a memory_limit (often 128 MB). When a request tries to go over it, PHP doesn’t politely return an error up the call chain. It uses a C feature called longjmp(), which is like pulling a fire alarm: execution stops wherever it is and jumps straight to the exit routine. PHP calls this a bailout.

Fire alarms are great for getting out fast. The problem is that everything halfway done stays halfway done. If PHP was in the middle of setting up a data structure (“I’ve reserved the memory, I’ll fill it in next line”), the alarm goes off between those two steps. The exit routine then walks past that half-built structure and trusts it.

That’s the whole story of PHP2Shell in one sentence: the emergency exit leaves half-built or freed things behind, and something later treats them as valid.

Step zero: how an attacker makes PHP run out of memory

Both bugs need a crash in a specific place, usually an out-of-memory bailout. You might think memory_limit plus post_max_size (the cap on request body size) would make that impossible from outside. The advisory explains several ways the attacker can still get there, before any application code runs, because PHP parses uploaded form data (multipart/form-data) automatically:

  • No usable Content-Length. A multipart request that doesn’t declare a usable body length can slip past the initial post_max_size check while still delivering body data.
  • Unlimited headers per part. Each part of a multipart body can carry as many headers as the attacker likes. On the wire they’re small, but inside PHP each one costs a lot more memory. Enough of them blow through memory_limit while staying under post_max_size.
  • Empty file fields are free. Empty file inputs don’t count toward max_file_uploads, but PHP still builds a metadata array for each one. Nested field names (like a[b][c][d]...) multiply the work.
  • App-specific allocations. Anything in an app that builds large structures from input can also reach the limit.

In other words, the attacker can choose to trigger an out-of-memory bailout at a moment of their choosing, with one ordinary-looking HTTP request. That’s the trigger. The two CVEs are about what that bailout leaves behind.

CVE-2026-109618: RCE via out-of-memory corruption

The flaw. In some places, PHP makes request memory reachable before it is fully initialized. If any allocation fails at that moment, even an unrelated one, the bailout fires. Later, request shutdown reads or acts on the half-initialized memory as if it were valid. The PHP team’s wording: request cleanup “may then treat the invalid data as valid.”

Why that’s dangerous: “uninitialized” doesn’t mean “empty.” It means “whatever was there before.” In a reused worker, what was there before can be data the attacker sent in an earlier request. So cleanup code may free or follow pointers the attacker effectively wrote.

Where it bites hardest: Apache mod_php. mod_php runs request shutdown even when request startup failed. Since body parsing happens during startup, an attacker can exhaust memory during parsing, and then shutdown walks over the broken state, all before WordPress or Drupal ever loads. That’s the case the PHP team confirmed as exploitable: full RCE over HTTP with default memory and input limits, on normal prefork setups with multiple workers.

How the other server types compare, per the advisory:

Server type CVE-2026-109618 status
Apache mod_php Confirmed RCE, application-independent
PHP-FPM A failed startup kills the child, which limits this. OOM after normal startup may still reach the bug. Analysis says susceptible; no full FPM exploit shown.
FrankenPHP Likely susceptible to app-independent attacks (calls shutdown unconditionally)
LiteSpeed (LSAPI) Modeled on FPM; app-specific exploits likely but unproven

CVE-2026-109619: RCE via stale global state

The flaw. Remember the global state that’s supposed to be reset after every request? Some of it points into request memory: the last error, session bookkeeping, objects, extension data. When the request ends, the request memory is thrown away but some of those global pointers are not reset. They now point at freed memory. If code in the next request reads them before normal initialization resets them, that’s a use-after-free.

The PHP advisory frames it neatly as producers and consumers:

Producers leave stale state behind:

  • Fatal errors that cut an operation short: parse failures, uncaught errors, failed require, and “Maximum execution time exceeded” (so a timeout counts too).
  • Out-of-memory bailouts after error, exception or extension state was already recorded.
  • Warnings emitted late during shutdown that re-fill error state after it was cleared.

Consumers read that stale state early in the next request:

  • Configuration warnings at request start. If a per-request setting (for example from .htaccess or Apache php_value) produces any warning, notice or deprecation before the request is initialized, PHP touches the stale error state. This happens even if the message is hidden. @, error_reporting and turning off logs don’t help.
  • Session upload progress. This feature (more below) can touch stale session or decoder state left by an interrupted request.

Confirmed paths:

  1. PHP 8.5 fatal_error_backtraces. PHP 8.5 added backtraces to fatal errors. That backtrace is request memory but was never reset at shutdown. The next diagnostic reaches the stale value. This is the main demonstrated RCE against mod_php.
  2. Session upload progress. An OOM or fatal error during upload processing leaves session globals pointing at freed memory, and a later upload-progress operation uses them. Validated on both mod_php and PHP-FPM, enabled by default, and the app doesn’t need to call session_start().

The catch for the attacker: the producer request and the consumer request must land on the same worker. More workers and worker recycling lower the odds per attempt, but they don’t remove the bug. The attacker just sends more requests.

The hidden trigger: session upload progress

This feature deserves its own section because it’s what makes several attack paths application-independent.

Since PHP 5.4, if a form upload includes a field named after session.upload_progress.name (by default PHP_SESSION_UPLOAD_PROGRESS), PHP writes upload progress into the session while it’s still parsing the body. It was built so sites could show upload progress bars. It’s on by default.

From an attacker’s view, that means:

  • It runs during body parsing, before any application code.
  • It touches session machinery even if the app never uses sessions.
  • It’s reachable with one unauthenticated POST to almost any PHP URL.

Three of the five techniques Wiz and Imperva describe use it as the remote trigger. That’s why disabling it is the most commonly recommended stopgap.

What the researchers actually built

Wiz Research reported four concrete bug paths under the two CVEs (Imperva lists a fifth crash-only variant). In plain language:

# Technique CVE What it gives the attacker
1 Upload-progress unserializer UAF. OOM interrupts session decoding during upload progress, leaving globals pointing at freed memory. 109618 Memory corruption
2 Session ID memory disclosure. OOM during early multipart parsing leaves the session ID pointer pointing at freed memory. A later request trusts it and its length, and sends whatever is there back in the PHPSESSID cookie. 109618 Reads ~67 KB of worker memory per hit, from any PHP app
3 Fatal-error backtrace UAF (PHP 8.5). The stored backtrace isn’t cleared after an OOM crash; the next request touches it. 109619 Memory corruption
4 OPcache type confusion. OPcache keeps compiled code in memory shared by all workers. An allocation failure while saving leaves one worker’s view of a slot out of sync, so another worker reuses it for a different type of object. 109619 Shared-memory corruption; Imperva also describes an arbitrary file read variant

A note on the CVE column: the session upload-progress path shows up under both CVEs depending on the source. That’s not a contradiction. It is an OOM (CVE-2026-109618) that leaves session globals stale (CVE-2026-109619). The two CVEs are two halves of the same failure pattern, which is why PHP fixed and disclosed them together and why you need the patched release, not one mitigation.

From bugs to a shell, conceptually

None of those alone is “run my code.” Here’s how they combine, at the level of strategy:

 [1] Trigger            [2] Leak                 [3] Groom               [4] Corrupt
 Crafted multipart  ->  Session-ID disclosure -> Follow-up requests   -> PHP follows a stale
 request forces OOM     returns raw memory in    place attacker data     pointer into attacker
 bailout; cleanup       PHPSESSID cookie, so     into the freed spots    data -> control of
 is skipped             attacker learns layout   on the same worker      execution -> shell
  1. Trigger. Force an OOM or fatal error at a precise point so cleanup is skipped and stale pointers remain.
  2. Leak. Modern systems randomize where things live in memory (ASLR), so blind corruption usually just crashes. The disclosure bug fixes that for the attacker: it hands back raw worker memory, revealing addresses and layout. As a bonus, that memory can contain other users’ data: passwords, session tokens, API keys, certificates.
  3. Groom. Send follow-up requests to the same worker that allocate attacker-chosen data of the right size, so it lands exactly where the stale pointer points. (Security folks call this heap grooming.)
  4. Corrupt. When PHP later uses the stale pointer, it’s reading attacker-built structures. With the addresses from step 2, that becomes control of what the process does next, which means running commands.

The code runs as the web server user (like www-data), not automatically as root. But that user can read your app’s config, database password, and usually write to the web root. That’s plenty.

Why CVSS says “high complexity” but you should still panic

The vector is AV:N/AC:H/AT:P/PR:N/UI:N/VC:H/VI:H/VA:H. Translated:

  • AV:N network, PR:N no login, UI:N no victim click.
  • AC:H high complexity: the exploit has to be tuned to the PHP build, extensions and memory layout.
  • AT:P attack requirements present: the right server type, the producer and consumer hitting the same worker.
  • VC/VI/VA:H total loss of confidentiality, integrity and availability.

“High complexity” used to mean “only a few elite teams will pull this off.” Wiz’s own point is that this no longer holds: they used AI tools for discovery and exploit development, and adapting the exploit to new versions and layouts took hours, not months. The memory-disclosure path also needs far less tuning than full RCE, and it alone can leak credentials.

Found with AI, and why that matters

Wiz says it found these bugs with AI-assisted research: its own tooling plus frontier models from Google and OpenAI, used for discovery, exploit development and patch validation (an OpenAI researcher is among those credited on the advisories). The first bug was found on 19 August; working pre-auth RCE on Drupal followed the next day.

The bugs had been sitting in PHP for over a decade. They weren’t hidden; they were just tedious to find and very hard to turn into a reliable exploit. Wiz’s argument is that AI collapses exactly that tedium: adapting an exploit to a new PHP build, configuration or memory layout went from a specialist’s weeks to hours. For defenders, the takeaway is uncomfortable but useful: “too complex to exploit in practice” is a weaker comfort than it used to be, and patch windows for critical bugs are shrinking accordingly.

Who is exposed

Fix status by PHP branch: 8.5, 8.4 and 8.3 have fixed releases; 8.2 is fixed but reaches end of life in December 2026; 7.4 to 8.1 need vendor backports; 7.3 and older have no fix

By PHP version

Branch Status What to do
8.5 Supported Update to 8.5.12
8.4 Supported Update to 8.4.27
8.3 Security fixes only Update to 8.3.36
8.2 Security fixes only (EOL 31 Dec 2026) Update to 8.2.35, and plan to move off 8.2
8.1, 8.0, 7.4 End of life Minimal source backports from PHP; get a vendor/distro build, or upgrade
7.3 and older End of life No fix. Upgrade, isolate, or apply every mitigation below

By scale. Recorded Future cites scans showing about 4.4 million PHP services on 1.9 million hosts, with about 79% of version-reporting services on unsupported branches. PHP 7.4 alone accounts for roughly 880,000 services. Only about 85,000 IPs were running the newest pre-fix releases. These are exposure counts, not confirmed-vulnerable counts, but they show the problem: much of the internet runs PHP versions that will never get an official fix.

PHP you didn’t know you had. PHP is embedded in appliances and products: firewall and router web UIs, hosting panels like cPanel and Plesk, Windows software, container images. SANS notes advisories from vendors including Cisco, OPNsense, pfSense and Palo Alto Networks. The Dutch NCSC keeps a community list of affected products (opens in a new tab).

Timeline

Timeline from Wiz’s discovery on 19 August to the leak, first attack reports and patch release on 21 October

All times UTC. Sources disagree by a few minutes on the exact release time; PHP had scheduled 14:00 and shipped slightly early.

Date Event
2012 PHP 5.4 adds session upload progress (on by default)
2024 External audit of PHP surfaces signs of the OOM bug; fix stalls over trade-offs (per PHP Foundation)
11 Aug 2026 Calif reports the bugs to PHP (per PHP Foundation)
19 Aug 2026 Wiz finds the first bug, using AI-assisted research
20 Aug Unauthenticated RCE confirmed on Drupal
21 Aug Unauthenticated RCE confirmed on WordPress (later Joomla too)
24 Aug Reported privately to the PHP security team
21 Sep PHP acknowledges and starts building fixes
17 Oct Amazon’s honeypots catch an exploitation attempt, four days before disclosure
20 Oct PHP pre-announces a critical release for 21 Oct, 14:00, reserving the right to ship early if details leak
21 Oct, ~02:00 Amazon sees multiple PoC-based attempts from different attackers
21 Oct, 02:29 An archive with vulnerability details is first uploaded to VirusTotal: the leak
21 Oct, 03:02 Cloudflare turns on WAF rules by default
21 Oct, 07:34 Patchstack reports multiple active exploitation incidents from hosting partners
21 Oct, 07:41 Chainguard publishes patched images ahead of the official release
21 Oct, ~13:30–13:52 PHP 8.5.12, 8.4.27, 8.3.36, 8.2.35 released with advisories
21 Oct CISA and NSA urge immediate patching; SANS ISC raises its threat level to yellow; PHP Foundation publishes its statement; public “PoCs” appear
22 Oct Cloudflare reports millions of attempts; Amazon discloses the 17 Oct probe; no confirmed compromise reported yet; not in CISA KEV

About those public PoCs. Many GitHub repos claim to be PHP2Shell exploits. Researchers report that quality varies wildly; at least one widely shared author later retracted the theory their PoC was based on. Don’t run random exploit code against anything, and be suspicious of “scanner” repos, which are a classic malware lure after a big disclosure.

How the fix works

The patches change PHP’s design, not just one line:

  • No more longjmp() on out-of-memory. Instead, PHP uses a soft OOM: when the limit is hit, it waits until the next safe point in execution and stops gracefully, so nothing is left half-built. If memory keeps climbing to a second, higher threshold, a hard OOM kills the process outright with _exit(), which is safe because a dead process can’t reuse stale state.
  • Full reset of request-owned global state (error, session, object and extension state) before request memory is freed, so nothing points into freed memory when the next request starts.

Normal memory_limit behavior is preserved, so apps shouldn’t notice.

Detection: did someone try this on me?

The PHP team and Wiz both warn that this happens below the application layer, so app logs (WordPress audit plugins, etc.) may show nothing. Look at the web server, PHP and system level instead. Hunt back to at least 17 October 2026, the date of the earliest known attempt.

Request-level signs

  • POST requests with multipart/form-data that are unusually large (the circulating PoC shape was around 5 MB) or have huge numbers of fields, parts, headers or nested names.
  • A PHP_SESSION_UPLOAD_PROGRESS field on endpoints that have no business receiving uploads.
  • Abnormally long filenames inside multipart bodies.

PHP and server signs

  • Bursts of Allowed memory size of ... bytes exhausted errors.
  • Repeated HTTP 500s, segfaults, or worker crash and respawn churn.
  • Unusually large PHPSESSID values in responses. A normal session ID is a few dozen characters; the disclosure bug returns kilobytes.

Post-exploitation signs

  • httpd, apache2, php-fpm or php-cgi spawning shells (sh, bash), interpreters, or download tools like curl and wget. Alert on this regardless of PHP2Shell.
  • New or modified files in the web root (web shells), new cron jobs or scheduled tasks, new accounts, unexpected outbound connections.

If you find signs of compromise: isolate the server, preserve web and PHP logs, and assume anything the PHP user could read is exposed: database credentials, API keys in config files, session secrets. Rotate them. Because the disclosure bug can leak other visitors’ data from memory, consider invalidating user sessions too. Rebuilding from a known-good image is safer than cleaning in place.

Five-minute self-check

Run these on your own server (they’re read-only):

  1. Which PHP does the web server use? php -v shows the command-line PHP, which can differ from the web one. Also check your package manager (apt list --installed | grep php or rpm -qa | grep php) and, on FPM, php-fpm -v (the binary may be named php-fpm8.3 or similar).
  2. Is the version at or above the fix for its branch? 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35. On a distro package, compare against your distro’s advisory instead, since backported fixes keep the old version number.
  3. Is upload progress on? php -i | grep session.upload_progress.enabled (repeat for the FPM/Apache config if it differs).
  4. Which SAPI? If you see libapache2-mod-php or mod_php loaded in Apache, you’re in the highest-risk group.
  5. When did PHP workers last restart? If it was before you updated, they’re still running the old code.

What to do

Four-step response plan: update, restart PHP workers, verify the version the web server runs, then hunt for signs of compromise back to 17 October

1. Patch, then restart

  • Update to 8.5.12, 8.4.27, 8.3.36 or 8.2.35 or your distribution’s fixed package. Linux distros (Ubuntu, Red Hat, AlmaLinux, Debian and others) got patches in advance; some backport to older PHP versions, so check your distro’s advisory rather than the version number alone.
  • Restart PHP-FPM and/or Apache. Running workers still have the old code loaded. Updating the package without restarting fixes nothing.
  • Verify: php -v on the command line can differ from what the web server runs. Check through the web server (for example a temporary, access-restricted phpinfo() page you delete afterwards) or your package manager.
  • Find the PHP you forgot about: appliances, panels, containers, staging boxes. Rebuild container images; a FROM php:8.3 image built last month is still vulnerable.

Shared hosting? You can’t patch PHP yourself. Ask your host to confirm they’ve updated and restarted, and check the PHP version in your control panel.

2. If you truly can’t patch yet

None of these make you safe; PHP’s release notes say “There are no complete mitigations, updating is the only safe choice.” They shrink the attack surface while you get there. Apply them in php.ini, the FPM pool or the server config (not via ini_set() or .user.ini, which run too late), and restart every worker.

Against CVE-2026-109619 (stale state)

  • session.upload_progress.enabled = 0. Blocks the confirmed session paths. Worth keeping off even after patching if you don’t use upload progress bars.
  • On PHP 8.5: fatal_error_backtraces = 0.
  • Remove invalid or deprecated per-request settings (.htaccess, php_value) that generate warnings at startup.
  • Fix fatal errors and timeouts attackers can reach (upload/import handlers, callbacks, cleanup code).
  • If performance allows, pm.max_requests = 1 on FPM so each worker handles a single request.

Against CVE-2026-109618 (OOM corruption)

  • Set memory_limit = -1 in a way apps can’t override (php_admin_value). This removes the PHP-level OOM trigger, but real system allocation failures can still hit the bug, so pair it with:
  • A container or cgroup memory limit as the safety net. Do not use RLIMIT_AS or RLIMIT_DATA: they make allocations fail, which triggers the very bug.
  • Low post_max_size: 8M with no uploads, 32M with uploads; keep upload_max_filesize below it.
  • At the reverse proxy or web server, limit decoded body size, field count, multipart part count, headers per part, and nesting depth, including for requests without a usable Content-Length. Lower max_input_nesting_level and max_input_vars in PHP too.

Network-level

  • If your app doesn’t take uploads, block multipart/form-data at the web server or WAF.
  • Put the site behind a WAF; Cloudflare, Akamai, Fastly, AWS, Imperva and others published guidance or rules. Make sure the origin server can’t be reached directly, bypassing the WAF.
  • Take admin panels and appliance web UIs off the public internet.
  • For anything on PHP 7.3 or older: isolate it or put it behind a VPN until it’s upgraded.

3. Lessons for developers

  • Error paths are attack surface. The happy path was fine for a decade. The bugs were in what happens when things fail halfway. Test and fuzz your failure handling, not just your features.
  • Emergency exits must leave things consistent. If you use longjmp, exceptions, or early returns, ask: “what’s half-built if we leave right here?”
  • Reset per-request state completely in long-lived workers. This applies equally to Node, Python and Java servers that keep globals across requests.
  • Pre-application parsing is part of your attack surface. Your framework never saw these requests. Defense in depth at the proxy matters.
  • Running an unsupported runtime is a decision to accept bugs like this unpatched. 79% of version-reporting PHP services are on end-of-life branches.

The bottom line

PHP2Shell isn’t a typo in a WordPress plugin. It’s a design-level flaw in how the engine under a large share of the web handles failure, and the core of it (upload progress plus the OOM bailout) has been there since 2012. A crafted upload makes PHP run out of memory, the emergency exit leaves pointers to freed memory behind, and the next request trusts them. The fix exists, it’s free, and it takes one package update plus a restart. The real risk is the long tail: the forgotten 7.4 server, the firewall’s admin page, the container nobody rebuilt. Find yours before someone else does.


Running PHP somewhere? Check which version yours is running today. Next up: a hands-on guide to auditing your server for end-of-life software.


Sources

Credits: discovered and reported by Wiz Research (credited on the PHP advisories as sagitzadik-wiz, tomash-areion, lcfr-hax and amyb-oai, the latter two linked to Calif and OpenAI per Recorded Future). Fixed by PHP developers arnaud-lb, iluuu1994, bukka and ndossche.