Antwa CodeAntwaCode Blog

Engineering18 min read

PHP2Shell: A Complete Guide to PHP's Critical RCE Flaws — News, Root Cause, and How to Secure Your Server

Two critical RCEs in the PHP engine itself (CVE-2026-109618 & CVE-2026-109619) are now being exploited at scale. A complete guide: the news, the Zend engine root cause, how to check & patch your server, temporary mitigations, and attack detection.

Read in Bahasa Indonesia →

php-src repository Image: GitHub — the php/php-src repository, where the PHP2Shell bug lived for over 7 years

The PHP world was recently rocked by two critical vulnerabilities dubbed PHP2Shell. This isn't a bug in the Laravel framework, not in a WordPress plugin, and not in your application code — it's in the PHP engine itself. That means even a "Hello World" site that just echoes one sentence can be attacked in exactly the same way as a complex enterprise application.

These two flaws — CVE-2026-109618 and CVE-2026-109619 — let an attacker with no account, no session, and no interaction at all run system commands on your web server through a single ordinary HTTP request. Both are rated CVSS v4.0 9.2 (Critical).

In this article we'll cover everything in full: what happened, the dramatic timeline of the leak, a technical teardown of the root cause in the Zend engine, the list of affected versions, and the practical steps to patch and secure your server.

What Is PHP2Shell?

PHP2Shell is the collective name given by security researchers — not by the PHP team — to two remote code execution (RCE) vulnerabilities in the PHP core runtime:

  • CVE-2026-109618 — RCE via OOM corruption: out-of-memory (OOM) handling can abort PHP execution mid-flight while internal request data is only partially initialized. The subsequent cleanup then reads that not-yet-valid data as if it were legitimate.
  • CVE-2026-109619 — RCE via stale global state: a request's internal state can get "left behind" after its memory is freed. The next request handled by the same worker uses that stale state before normal initialization gets a chance to reset it (use-after-free).

Both were discovered by teams from Wiz Research, OpenAI (Codex Security), and Calif, and reported through coordinated disclosure to the PHP security team. Interestingly, both Wiz and Calif say they used AI models in their research — and the Google Gemini team was also involved in the fix process.

Key facts:

  • Unauthenticated RCE — just one anonymous POST request, no login, no CSRF token, no application input.
  • It doesn't matter how clean your application code is — the attack happens before a single line of application code is compiled.
  • Extremely silent — the worker doesn't crash, the server returns HTTP 200, and the application log is clean.
  • Affects nearly every PHP version from 5.4 to 8.5. The bug has been in the PHP code for over 7 years.
  • Beyond RCE, an attacker can also read the PHP process's memory, which may contain passwords, API keys, tokens, and certificates belonging to other requests.

Timeline: Leaked on Hacker News, Release Moved Up a Day

The story behind PHP2Shell is quite a ride. Here's the timeline:

  • August 2026 — Researchers from Wiz, OpenAI Codex Security, and Calif discover both vulnerabilities and start working with the PHP security team to prepare fixes. Details are kept tightly under embargo.
  • October 20, 2026 — A post on Hacker News leaks the two CVE identifiers, claiming a CVSS score of 10.0 and impact on PHP version 5 and up, backed by nothing but "source: trust me, bro". The PHP team immediately issues an advance notice that same night (19:33 UTC) and schedules the security release for October 21 at 14:00 UTC — a day earlier than the original disclosure date of October 22.
  • October 21, 2026, 03:02 UTC — Cloudflare has already turned on emergency WAF rules for all of its customers (including the free tier) before the patch itself is even released.
  • October 21, 2026, a few hours before release — Hadrian reports PHP2Shell exploitation "in the wild at scale", mostly abusing the session upload progress path.
  • October 21, 2026, 14:00 UTC — PHP releases emergency patches: 8.5.12, 8.4.27, 8.3.36, and 8.2.35, complete with technical advisories for both CVEs.
  • October 21, 2026, 14:19 UTC — watchTowr confirms seeing "active exploitation attempts" for both vulnerabilities. Cloudflare reports that attackers keep rotating payloads to get past their firewall rules.
  • October 21, 2026 — CISA publishes the alert "Critical Vulnerabilities in PHP Require Immediate Patching".

A note about the score: the CVSS 10.0 claim circulating on Hacker News is not the official score. The PHP team rated both vulnerabilities 9.2 on CVSS v4.0. Still critical, still needs patching right away, but let's use the official numbers.

As of this writing, there's no confirmed report of a successful break-in to any specific server — but the volume of attack attempts is already massive. The principle is simple: if you haven't patched, assume your server is already within scanner range.

Why PHP2Shell Is So Dangerous

1. The Bug Isn't in Your Code

This is the most important and most frightening point. PHP processes incoming request data before extensions are initialized and before a single line of application code is compiled. So the exploitation is completely independent of whatever your server is hosting.

Is your application written perfectly, security-audited, and running the latest framework? Still hit. Is your server still running a rickety CMS abandoned by its developer five years ago? Also hit, with exactly the same degree of exposure. As CrowdStrike puts it: "A meticulously maintained application is exposed to precisely the same degree as a neglected one."

2. The Attack Is Silent

An exploited worker does not crash. The server returns a normal HTTP 200 response. The application log records nothing unusual. Even when errors occur, PHP fatal errors aren't always logged where you'd look. This makes log-based detection hard and lets attackers try over and over without getting caught.

3. One Request Is Enough

For mod_php, a proven-working exploit needs just one anonymous POST request. No session, no authentication, no user interaction. N-day scanning just fires payloads at every target and sees which ones "bite".

4. The Impact Cuts Both Ways

Besides running commands (RCE), CVE-2026-109619 has also been shown to allow memory disclosure — reading the contents of a PHP worker's memory. Why is that dangerous? Because worker memory can hold database credentials, API keys, authentication tokens, and certificates in use by other requests that happen to pass through the same worker. So even before an attacker gets a shell, they may already have your passwords.

5. All SAPIs Are Potentially Affected

According to the official PHP advisory, every PHP installation that serves web requests is at risk, including PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP, and LiteSpeed. The highest risk is with mod_php — for CVE-2026-109618, an exploit requiring no application at all has been proven to work on mod_php. PHP-FPM behaves differently (the worker is killed immediately if a request fails during early setup), so one exploit path doesn't carry over directly, but the advisory stresses that FPM remains susceptible, and for CVE-2026-109619 RCE exploitation has been validated on both mod_php and FPM.

Technical Teardown: The Root Cause in the Zend Engine

This section goes a bit deep. If you just want to know how to patch, feel free to skip ahead to the mitigation section — but understanding the root cause shows us how a bug like this could slip through for more than a decade.

Both vulnerabilities live in Zend, the C implementation at the heart of PHP: it compiles PHP source into bytecode, executes it on a virtual machine, and manages everything the language needs at runtime — variables, arrays, objects, reference counting, and memory allocation. Zend isn't an optional component or a third-party add-on; it ships with every PHP build (that's why its source sits in the Zend/ directory of php-src). A bug in Zend = a bug in the PHP engine itself, affecting every application, framework, and extension running on that interpreter.

Two Zend subsystems sit at the center of the problem: HashTable (which backs every PHP array, including the $_POST superglobal) and the Zend Memory Manager (Zend MM), the per-request allocator that sits between PHP and the operating system.

zend_bailout() and longjmp(): A Dangerous Shortcut

When PHP faces a fatal condition — memory_limit exceeded, maximum execution time passed, or the client connection dropped — PHP doesn't propagate an error up the call stack the way most languages do. PHP raises a fatal error and calls zend_bailout(), which performs a LONGJMP to abandon the current execution context entirely:

// Zend/zend.c
ZEND_API ZEND_COLD ZEND_NORETURN void _zend_bailout(const char *filename, uint32_t lineno)
{
    ...
    CG(unclean_shutdown) = 1;
    ...
    LONGJMP(*EG(bailout), FAILURE);
}

The upshot: no code between the fatal error and the surrounding zend_catch block gets executed. The C function that was halfway through building a data structure is simply thrown away: destructors don't run, bookkeeping isn't rolled back, and nothing records that the structure is incomplete. PHP then proceeds into a cleanup sequence that assumes every structure is in a consistent state.

That assumption is what breaks. And that's where both CVEs live.

CVE-2026-109618: Cleanup Reads Memory That Was Never Filled

This CVE covers contexts that leave request memory in an uninitialized state. The pattern looks roughly like this:

  1. A data structure is marked as already "holding" an entry before the memory for that entry is allocated.
  2. That allocation is what turns out to blow past memory_limit — so OOM happens, zend_bailout() is called, and execution jumps out.
  3. The entry is left holding stale heap data (leftover memory that the attacker can control).
  4. The failed request's cleanup routine reads those bytes and branches on them — this is where the attacker can steer execution.

The strategy for triggering the OOM itself is remarkably clever. Several multipart/form-data parsing quirks can drive a request into OOM while still appearing to stay within PHP's configured limits:

  • A multipart request without a valid Content-Length can pass the initial post_max_size check and keep feeding decoded body data.
  • A multipart part can contain an unlimited number of headers. PHP's representation of those headers can consume far more memory than their on-the-wire representation — so a request that "fits" within post_max_size can still exhaust a normal memory_limit.
  • Empty file controls don't count toward max_file_uploads, yet still create the usual file metadata arrays. Add nested field names and the array-building work multiplies.

The bug underlying this CVE — a specific HashTable ordering — came in through an October 2017 performance optimization and first shipped in PHP 7.3.0. The OOM path itself is even older.

The fix: the patch removes longjmp() for OOM. Instead, PHP now defers a graceful soft OOM to the next safe VM point, plus a hard OOM that calls _exit() directly if memory consumption hits a second, higher threshold. With this, the shutdown procedure never sees invalid state again.

CVE-2026-109619: Global State Left Behind Across Requests

This CVE covers contexts that leave global state in an invalid condition. PHP has various kinds of global state — errors, sessions, objects, and extension state — held by the worker but actually pointing into memory owned by a single request. That state must be reset before the request's memory is released.

The problem: some input, configuration, session, and error-handling operations run before the next request's normal initialization. Those operations accidentally access stale state that hasn't been reset yet — and a use-after-free happens.

The exploit needs two roles:

  • Producer: a PHP operation that saves state from one request into a location PHP can reach later, on the next request.
  • Consumer: an operation on the next request that reads, frees, or branches on that stale state before normal initialization resets it.

State can persist for several reasons: a fatal error (including one caused by running out of memory) can leave an operation before it performs its matching reset; even a non-fatal warning can refill error state after it was cleaned up during shutdown.

Two confirmed consumers:

  1. INI diagnostics before request initialization — warnings/notices/deprecations triggered while applying invalid or deprecated INI settings, before normal executor initialization. Examples: auto_detect_line_endings=1, weird filter.default values, legacy assert.* settings, mbstring.http_input, invalid date.timezone, and so on. Important: hiding errors with error_reporting, @, or turning off display_errors does not protect you — the global state is accessed before PHP decides whether to report anything.
  2. Session upload progress — this feature, enabled by default, can reach session handling before normal session initialization, so it can consume session/decoder state left behind by an interrupted request. This is the most exploited path in the wild attacks.

The two most confirmed cases: fatal_error_backtraces in PHP 8.5 (when enabled, fatal error reporting stores request-owned backtrace state and its reset is always missed during shutdown) and the session upload progress lifecycle (without needing session_start() at all!).

The fix: the patch performs a broader reset across all request-related state, so nothing survives across the request boundary. For PHP 8.5, fatal_error_backtraces is also made safer.

Affected and Safe Versions

The vulnerable window has been open for more than seven years across PHP releases. No branch is immune:

PHP BranchVulnerableFixed Version
8.5below 8.5.128.5.12
8.4below 8.4.278.4.27
8.3below 8.3.368.3.36
8.2below 8.2.358.2.35
7.4 – 8.1AllNo official release; minimal source backport available from the PHP team
5.4 – 7.3AllEOL, no fixes from the PHP project

A few notes:

  • Linux distros and LTS vendors generally receive patches earlier, so check your package repo — it's very likely already available.
  • PHP 8.2 goes end of life on December 31, 2026 and will receive no further security fixes after that. If you're still on 8.2, plan your upgrade.
  • For those building from source or running EOL PHP (7.4–8.1), the PHP team provides minimal backport branches: PHP-7.4-CVE-2026-109618-109619, PHP-8.0-..., and PHP-8.1-... in the php-src repository.
  • If you depend on rpms.remirepo.net (volunteer-maintained), PHP recommends moving to distro packages or building from source so you can update promptly.

How to Check the PHP Version on Your Server

Before patching, first check the version that actually serves your web requests.

Don't Get Confused: CLI vs FPM vs mod_php

This is the most common trap! The php -v command in your terminal shows the PHP CLI version, which may differ from the version your web server uses. Updating only the CLI package does not automatically update the PHP-FPM service or the Apache module.

# PHP CLI version (not necessarily what the web uses!)
php -v
 
# Running PHP-FPM version
php-fpm8.3 -v        # or php-fpm8.4, depending on version
systemctl status php*-fpm
 
# Check which PHP module Apache is using
apache2ctl -M | grep php
 
# Check the version the web server sees — the surest way!
echo "<?php phpinfo();" | sudo tee /var/www/html/info.php
curl -s http://localhost/info.php | grep -i "PHP Version"
sudo rm /var/www/html/info.php

Check Installed Packages (Debian/Ubuntu)

dpkg -l | grep -i php | awk '{print $2, $3}'

Check Installed Packages (RHEL/Fedora)

rpm -qa | grep -i php

How to Patch

Debian / Ubuntu

sudo apt update
sudo apt install --only-upgrade php* -y
 
# Restart ALL PHP services that might be running
sudo systemctl restart apache2          # if you use mod_php
sudo systemctl restart php8.3-fpm       # adjust version
sudo systemctl restart nginx
 
# Verify
php-fpm8.3 -v

RHEL / Fedora / Rocky / AlmaLinux

sudo dnf update php* -y
sudo systemctl restart httpd            # or php-fpm / nginx

Build from Source

Download the latest source from php.net/downloads and rebuild. For EOL PHP 7.4–8.1, use the backport branch PHP-X.Y-CVE-2026-109618-109619 from the php-src repository.

After Patching

  1. Restart all workers — the patch does nothing for processes still running the old binary.
  2. Verify via phpinfo() from the web side, not from the CLI.
  3. If you run many servers/containers, don't forget Docker images based on PHP — rebuild the images.

Temporary Mitigations (If You Can't Update Yet)

Strong warning: straight from the official PHP advisory — "There are no complete mitigations, updating is the only safe choice." Every step below only reduces exposure on specific attack paths, not all of them. Treat this as a bridge, not a solution.

For CVE-2026-109618 (OOM corruption)

; Set in php_admin_value (FPM pool / Apache config) so applications can't override it
memory_limit = -1
 
; Keep post_max_size low
post_max_size = 8M          ; if the application doesn't need file uploads
; post_max_size = 32M       ; if file uploads are needed
upload_max_filesize = 4M    ; must be smaller than post_max_size
 
; Reduce input nesting depth
max_input_nesting_level = 32

Important details:

  • Set memory_limit=-1 administratively (using php_admin_value in the FPM pool or Apache config), then double-check that no pool/directory/application sets a finite limit back. This removes PHP memory-limit failures that the attacker can control — but allocation failures from lower layers can still reach the vulnerable path.
  • Enforce limits at the web server/proxy level for: decoded request size, number of input fields, number of multipart parts, number of headers per part, and nesting depth. These limits must also cover requests without a valid Content-Length.
  • If the application doesn't use multipart form data at all, block such requests at the frontend before they ever reach PHP.
  • Container/cgroup memory limits (memory.max) can serve as an availability guardrail, ideally with a policy that only kills the affected worker — for instance, one cgroup per worker.
  • Do NOT use RLIMIT_AS or RLIMIT_DATA as a substitute! PHP explicitly warns that these actually trigger allocation failures on the vulnerable path.

For CVE-2026-109619 (stale global state)

; Turn off session upload progress (enabled by default!)
session.upload_progress.enabled = 0
 
; PHP 8.5 only
fatal_error_backtraces = 0

Important details:

  • Set these in php.ini, the FPM pool, or server config applied before request parsing. Don't rely on ini_set() or .user.ini — those come too late. After setting them, restart all workers.
  • This blocks the confirmed session path, but not every variant. The advisory explicitly states that no configuration mitigation covers every operation.
  • Clean up invalid/deprecated INI settings applied per-request that trigger diagnostics before initialization. Hiding diagnostics with error_reporting, display_errors, or @ does not help.
  • Fix fatal errors and timeouts an attacker can reach, including failures in callbacks, upload/import processing, and cleanup.
  • If performance isn't a concern, consider pm.max_requests=1 in FPM to break the cross-request producer-consumer cycle.
  • The session upload progress feature itself is almost never used and barely tested — consider leaving it disabled even after patching.

Attack Detection and WAF Hardening

What You Can See in Logs

PHP2Shell attacks are indeed silent, but the advisory recommends an isolated audit with log_errors=On, a working error_log, and error_reporting=E_ALL in the startup config, then reviewing Apache, PHP-FPM, reverse proxy, and service logs for:

  • Warnings/deprecations/core errors repeating before the target script runs.
  • Warnings appearing during extension/server cleanup (after normal error state has been cleared).
  • Fatal, parse, and uncaught errors.
  • Predictable Maximum execution time exceeded errors.
  • Suspicious multipart/form-data requests with a PHP_SESSION_UPLOAD_PROGRESS field, an unusual number of headers in a single part, or dozens of empty file fields in one request.

Honest caveat: the absence of errors in logs is not proof of safety. Historical logs may have been collected without deprecation severity, and failures can occur before PHP gets a chance to write a message. Logs complement a static configuration audit — they don't replace it.

WAF Rules

  • Cloudflare has had default mitigations enabled for all customers (including the free tier) since before the patch released, and keeps updating rules as new vectors appear.
  • AWS WAF: AWS published a rule guide focused on multipart traffic, request body size, and session upload progress fields.
  • For self-hosted WAFs (ModSecurity, etc.): limit body size, part count, headers per part, and block requests without a valid Content-Length for endpoints that accept form uploads.

Key Lessons

1. Bugs can sleep for more than a decade. The oldest code path here (the session path) dates back to PHP 5.4 (2012). Every PHP release in the past 10+ years carried this bug. A reminder that "old and stable" code isn't necessarily defect-free — it may just be undiscovered.

2. Application security isn't enough. We're used to auditing application code, but the runtime underneath rarely gets touched. PHP2Shell proves it: a perfectly maintained application has exactly the same exposure as an abandoned one. Defense in depth must include patching the runtime, not just the code.

3. AI played a role in finding this class of bug. Both Wiz and Calif confirmed using AI models in their research, and the Gemini team helped with the fix. This is a new era: heavyweight C memory bugs that once required months of manual analysis can now be dug up with AI assistance — for defense and for attack alike.

4. Disclosure processes can get messy, and that must be anticipated. The Hacker News leak forced the release to move up a day. The PHP team responded well (advance notice + earlier release), but one thing is clear: the ecosystem must be ready for "n-day in hours" scenarios.

5. Only updating actually fixes it. Once again, from the PHP team's own words: there is no complete configuration mitigation. session.upload_progress.enabled=0 blocks the confirmed path but not every variant; memory_limit=-1 removes the attacker-controllable OOM trigger but not every possible allocation failure. Patch, restart, verify.

Conclusion

PHP2Shell (CVE-2026-109618 and CVE-2026-109619) is a stark reminder that threats can come from the layer we trust most — the programming language itself. With a single anonymous POST request, an attacker can run commands on your server, read memory full of secrets, and leave behind almost no traces.

Fortunately, the fixes are available and free. Your action list for today:

  1. Check the PHP version the web server uses (not the CLI!) — via phpinfo() from the web side.
  2. Update to 8.5.12 / 8.4.27 / 8.3.36 / 8.2.35 (or backported distro packages) as soon as possible.
  3. Restart all workers and verify the version again.
  4. If you can't update yet, apply the temporary mitigations above — while accelerating your update schedule.
  5. Check the logs for signs of exploitation attempts, and make sure your WAF is active.

Don't put this one off. This bug, hidden for 10 years, is now being actively exploited at massive scale across the internet. Patch today.

Quick FAQ

I'm on shared hosting, what should I do? Good news: this is your hosting provider's problem, not yours. Bad news: you can't force them to hurry. Open a support ticket mentioning both CVEs and ask when the PHP on your server will be updated to a safe version. In the meantime, make sure a WAF sits in front of your site (Cloudflare, for example) and watch your provider's announcements.

Is installing a WAF enough without updating? No. A WAF is an extra fence, not a locked door. Cloudflare itself reported attackers constantly swapping payloads to get past its rules. A WAF buys you time; the patch is what actually fixes the problem. The PHP advisory explicitly states that updating is the only complete mitigation.

I'm still on PHP 7.4 / 8.0 / 8.1, what now? These versions are end-of-life and will not receive official releases. The PHP team provides minimal source backport branches (PHP-7.4-CVE-2026-109618-109619, etc.) for those building from source. Some distros and LTS vendors also ship backported packages — check with your vendor. If neither option exists, it's time to plan an upgrade to a supported branch (8.2.35 or newer, but remember 8.2 goes EOL at the end of December 2026 — ideally jump straight to 8.3/8.4/8.5).

Is the PHP CLI on my server vulnerable too? The exploitation targets the SAPIs that serve web requests (mod_php, FPM, php-cgi, FrankenPHP, LiteSpeed). PHP CLI doesn't serve web requests, but you should still update it so your whole installation is consistent — and remember, the CLI package and the FPM service often update separately.

I use Laravel / Symfony / WordPress, am I safe because my framework is patched? It's irrelevant. The bug lives in the PHP interpreter, not in any framework. Any framework running on PHP has exactly the same exposure. What determines your risk is the PHP binary's version, not your framework's version.

How can I tell whether my server has already been exploited? Hard, because the attack is designed to be silent. What you can do: audit web server and PHP-FPM logs for suspicious multipart POST requests (especially ones carrying the PHP_SESSION_UPLOAD_PROGRESS field), check for suspicious processes running as the web server user, look for changed files in the web root, and audit any credentials that passed through that server (rotating passwords/API keys is a reasonable step after an incident). When in doubt, bring in an incident response team.

Why does memory_limit=-1 sound like a terrible idea? Intuitively it does sound wrong — wouldn't removing the memory limit open the door to DoS? But in this context, a finite memory limit is the attacker's trigger for reaching the OOM bug path. With memory_limit=-1 (applied administratively), the attacker loses that predictable trigger. The DoS risk is managed through other limits: a low post_max_size, frontend limits on part/field/header counts, and container/cgroup boundaries. Once the patch is in place, you're free to tune this limit back to whatever your workload needs.

Is updating the php package via apt/dnf enough? Not necessarily. Make sure EVERY service that runs PHP is updated and restarted: the Apache module (libapache2-mod-php*), PHP-FPM (php*-fpm), and don't forget containers/Docker images using a PHP base. The final verification is the same either way: open phpinfo() from the web side and check the version.

References

More posts