PHP has various global state that temporarily points into request memory
PHP has various global state that temporarily points into request memory. In some scenarios, global state may not be correctly reset, or may be repopulated after being reset. The global state may then refer to request memory even after it has been freed. Subsequent requests may read or branch on this stale state before it is fully initialized. Multiple strategies can trigger this condition and produce remote code execution (RCE).
* Cloud Computing
* [AI and API Manager](https://www.akamai.com/solutions/ai-and-api-manager)
* [Serverless](https://www.akamai.com/solutions/serverless)
* [Media](https://www.akamai.com/solutions/media)
* [SaaS](https://www.akamai.com/solutions/saas)
* [See all Cloud Computing](https://www.akamai.com/cloud)
* security
* [Frontier AI Security Risks](https://www.akamai.com/solutions/frontier-ai-security-risks)
* [AI Security](https://www.akamai.com/solutions/ai-security)
* [Akamai Application Protection Platform](https://www.akamai.com/solutions/akamai-application-protection-platform)
* [Cybersecurity Compliance](https://www.akamai.com/solutions/cybersecurity-compliance)
* [Ransomware Protection](https://www.akamai.com/solutions/ransomware-protection)
* [Secure Apps and APIs](https://www.akamai.com/solutions/app-and-api-security)
* [DNS Delivery and Security](https://www.akamai.com/solutions/dns-delivery-and-security)
* [Zero Trust](https://www.akamai.com/solutions/zero-trust-security)
* [DDoS Protection](https://www.akamai.com/solutions/ddos-protection)
* [Bot & Agent Control](https://www.akamai.com/solutions/bot-and-agent-control)
* [Identity, Credential and Access Management](https://www.akamai.com/solutions/identity-credential-and-access-management)
* [See all Cybersecurity](https://www.akamai.com/security)
* content delivery
* [App and API Performance](https://www.akamai.com/content-delivery-network/app-and-api-performance)
* [Media Delivery](https://www.akamai.com/content-delivery-network/media-delivery)
* [See all Content Delivery](https://www.akamai.com/content-delivery-network)
* industry solutions
* [Media and Entertainment](https://www.akamai.com/solutions/industries/media)
* [Retail, Travel, and Hospitality](https://www.akamai.com/solutions/industries/retail-travel-hospitality)
* [Financial Services](https://www.akamai.com/solutions/industries/financial-services)
* [Healthcare and Life Sciences](https://www.akamai.com/solutions/industries/health-care-life-sciences)
* [Public Sector](https://www.akamai.com/solutions/industries/public-sector)
* [Defense](https://www.akamai.com/solutions/industries/defense-and-intelligence)
* [Games](https://www.akamai.com/solutions/industries/games)
* [Online Sports Betting and iGaming](https://www.akamai.com/solutions/industries/online-sports-betting)
* [Service Providers](https://www.akamai.com/solutions/industries/service-providers)
* [See all Industry Solutions](https://www.akamai.com/solutions/industries)
* Pricing
Back Pricing Close
Security and Delivery
* Get started
* [Contact Sales](https://www.akamai.com/why-akamai/contact-us/contact-sales)
* [Free trials](https://www.akamai.com/products#free-trials)
On October 20, 2026, PHP announced that they would release a patch on October 21 at 14:00 UTC for two critical vulnerabilities.
These vulnerabilities are called PHP2Shell because they allow an attacker to achieve remote code execution (RCE).
The details were scheduled to be disclosed on October 22, but information about the vulnerabilities, including their CVE IDs (CVE-2026-109619 and CVE-2026-109618), was leaked early.
CVE-2026-109619 is a use-after-free (UAF) vulnerability in PHP core's upload progress feature.
CVE-2026-109618 is a memory corruption vulnerability triggered by an out-of-memory (OOM) condition.
This blog post provides the full details behind these two vulnerabilities.
The target runs PHP 5 onward with session.upload_progress.enabled = On (the default)
For any endpoint that accepts file uploads (even one that ignores them): PHP's session layer processes the upload progress before userland code runs
Attackers need the build-specific offset between zval_ptr_dtor and zend_call_function (either leaked at runtime or derived from the target's exact PHP binary/distro)
CVE-2026-109619 vulnerability details
There are 5 steps to exploiting the vulnerability:
Access the upload progress feature
Create the free condition
Trigger the free
Heap spray
Trigger the exploit
Zoom image
Close popover
Step 1: Access the upload progress feature
PHP has a feature called “upload_progress”, which is available in default builds. This feature provides real-time feedback on how much of a file has been successfully uploaded to the server.
When a user initiates a multipart/form data upload (described in RFC 1867) and includes a POST parameter named “PHP_SESSION_UPLOAD_PROGRESS”, PHP intercepts it before the target script even begins executing.
PHP immediately creates a specific key inside the $_SESSION array (a built-in superglobal associative array used to store and access user data across multiple pages during a single website visit). As the file uploads chunk by chunk, PHP continuously updates this session array with metrics like content_length (total size), bytes_processed, and the names of the files.
Under the hood, when PHP’s HTTP parser encounters the file field, it triggers the internal FILE_START event. To track the progress efficiently, PHP creates a zval pointer(i.e., a C memory pointer used internally in the PHP’s core) mapped directly to that session array in memory, allowing the C-level engine to rapidly write the incoming byte counts.
Let’s take a look at the struct “php _session_rfc1867_progress” from php_session.h in the source code of the PHP core:
It turns out that PHP caches two raw zval * pointers when processing a multipart upload:
zval *post_bytes_processed;
zval *current_file_bytes_processed;
So, when the attacker sends a POST request with:
PHPSESSID cookie/field — It sets the session ID
PHP_SESSION_UPLOAD_PROGRESS field — It activates the progress tracking
A file field — It triggers FILE_START, which caches both pointers
When a user sends a POST request that triggers the progress tracking, there are two zval pointers that point into live hash table buckets inside the decoded progress tree.
Step 2: Create the free condition
The attacker pauses the TCP stream for greater than 1 second (the default session.upload_progress.min_freq). This is needed to bypass PHP's rate limiting guard on progress updates.
Step 3: Trigger the free
At this point, before the second update(), the memory looks like this:
progress->post_bytes_processed → [ bucket in Tree-A ]
progress->current_file_bytes_processed → [ bucket in Tree-A ]
Tree-A is the live decoded session data, stored in $_SESSION under the key "upload_progress_x".
When data resumes, a second FILE_DATA event fires. Because enough time has passed, a second php_session_rfc1867_update() runs, and causes the following flow:
flush() writes Tree-A to disk and resets status
After flush, sets session_status = none
php_session_initialize() re-reads the session file
Because status is none, PHP treats this as opening a fresh session. It reads the file from disk and deserializes it into a brand new tree — let’s call it Tree-B. Tree-B is a completely new allocation at a different address. It contains the same data as Tree-A, but it's a copy.
php_session_track_init() destroys Tree-A
PHP now has two trees with the same session key:
Tree-A (the old one, still in $_SESSION)
Tree-B (the fresh one, just deserialized)
zend_hash_update() put Tree-B into $_SESSION. The hash table's pDestructor (zval_ptr_dtor) runs on the old value - Tree-A. This frees Tree-A and all its buckets.
Now the pointers are dangling: progress->post_bytes_processed and progress->current_file_bytes_processed they are freed.
Tree-B exists as a new address, but nobody updates the pointers.
So basically, PHP caches raw zval * pointers into a hash table, then destroys and replaces that hash table without updating the cached pointers. This causes UAF, which is the bug behind CVE-2026-109619.
Step 4: Heap spray
Immediately after the free, in the same HTTP POST body as the upload, the attacker places a lot (not one, to ensure saturation) form fields, all named t[]:
--boundary
Content-Disposition: form-data; name="t[]"
<288 bytes of forged struct>
--boundary
Content-Disposition: form-data; name="t[]"
<288 bytes of forged struct>
... (×149)
PHP's multipart parser (rfc1867.c) processes these as FORMDATA events. Each value becomes a zend_string in $_POST['t'], which is a PHP array. The name t[] tells PHP to append to an array, so all 149 land in $_POST['t'][0] through $_POST['t'][148].
Each tag contains a fake PHP internal function structure with system("<command>") baked in. These allocations are sized to land in the exact same memory slot that was just freed. Then, the allocator might hand the exact same slot of the data that just got freed, and the attacker’s payload can get inside, so the dangling pointer would point to the attacker’s payload.
Step 5: Trigger the exploit
The attacker sends a second file in the same POST. PHP writes through the dangling pointers into what it thinks is the progress structure:
But it's now the attacker's fake function. When PHP's engine touches this corrupted data, it follows the fake pointers and calls the system ("attacker command").
CVE-2026-109618 vulnerability details
In this vulnerability, the problem is that OOM forces PHP to abort execution in unexpected way, so normal cleanup can be bypassed, leaving uninitialized memory vulnerable to malicious overwrite and execution.
The attacker sends a maliciously crafted request designed to rapidly consume memory until it hits PHP's configured memory_limit. This can be achieved through massive file uploads.
Then, when the PHP’s core detects the OOM condition, it must halt execution. Instead of returning through function normally and clearing variables step by step, it uses longjmp() (the underlying C-level mechanism) to instantly unwind the stack and jump directly to the request shutdown mechanism.
Because longjmp() abruptly skips local cleanup and initialization code, certain internal structures are left in a volatile, uninitialized, or dangling state.
The attacker can take advantage of it, and before the OOM he can manipulate the heap (for example, by using techniques like sending a lot of “t[]” form fields, like we saw earlier).
This fills memory segments with forged C-structures containing malicious execution pointers.
Finally, During the php_request_shutdown phase, PHP systematically goes through active resources to free them. When it encounters the uninitialized memory left behind by the longjmp() interruption, the engine blindly reads the attacker's forged structure. If the engine branches on a function pointer (like a destructor) within that corrupted memory, execution jumps to the attacker's payload, resulting in RCE.
Mitigation with Akamai Guardicore Segmentation
Akamai ExAR customers have already received an incident detailing their affected assets, including the vulnerable PHP versions, the web server integration in use, and recommended mitigation and segmentation steps.
WITH LINUX AS (
SELECT DISTINCT name, version, source
FROM deb_packages
WHERE REGEX_MATCH(name, '^php[0-9][0-9.]*-(common|cli)$', 0) IS NOT NULL
UNION ALL
SELECT DISTINCT name, version, source
FROM rpm_packages
WHERE REGEX_MATCH(name, '^(rh-)?php[0-9.]*[uw]?(-php)?-(common|cli)$', 0) IS NOT NULL
),
PARSED AS (
SELECT name, source, version,
CAST(REGEX_MATCH(version, '([0-9]+)\.([0-9]+)\.([0-9]+)', 1) AS INTEGER) AS major,
CAST(REGEX_MATCH(version, '([0-9]+)\.([0-9]+)\.([0-9]+)', 2) AS INTEGER) AS minor,
CAST(REGEX_MATCH(version, '([0-9]+)\.([0-9) AS patch,
CASE
WHEN version LIKE '%8.5.9-0ubuntu2.%'
THEN CAST(REGEX_MATCH(version, '0ubuntu2\.([0-9]+)', 1) AS INTEGER) >= 1
WHEN version LIKE '%8.5.4-0ubuntu1.%'
THEN CAST(REGEX_MATCH(version, '0ubuntu1\.([0-9]+)', 1) AS INTEGER) >= 5
WHEN version LIKE '%8.3.6-0ubuntu0.24.04
THEN CAST(REGEX_MATCH(version, '24\.04\.([0-9]+)', 1) AS INTEGER) >= 13
WHEN version LIKE '%8.1.2-1ubuntu2.%'
THEN CAST(REGEX_MATCH(version, '1ubunt >= 28
WHEN version LIKE '%7.4.3-4ubuntu2.%'
THEN CAST(REGEX_MATCH(version, '4ubunt * 1000
+ COALESCE(CAST(REGEX_MATCH(versiGER), 0) >= 29005
WHEN version LIKE '%7.2.24-0ubuntu0.18.0
THEN CAST(REGEX_MATCH(version, '18\.04 1000
+ COALESCE(CAST(REGEX_MATCH(version, 'esm([0-9]+)', 1) AS INTEGER), 0) >= 17014
WHEN version LIKE '%7.0.33-0ubuntu0.16.0
THEN CAST(REGEX_MATCH(version, '16\.04\.([0-9]+)', 1) AS INTEGER) * 1000
+ COALESCE(CAST(REGEX_MATCH(versiGER), 0) >= 16022
WHEN version LIKE '%5.5.9+dfsg-1ubuntu4.
THEN CAST(REGEX_MATCH(version, '1ubunt * 1000
+ COALESCE(CAST(REGEX_MATCH(version, 'esm([0-9]+)', 1) AS INTEGER), 0) >= 29018
ELSE 0
END AS ubuntu_fixed
FROM LINUX
)
SELECT name, source, version
FROM PARSED
WHERE ubuntu_fixed = 0
AND (
(major = 8 AND minor = 5 AND patch < 12)
OR
(major = 8 AND minor = 4 AND patch < 27)
OR
(major = 8 AND minor = 3 AND patch < 36)
OR
(major = 8 AND minor = 2 AND patch < 35)
OR
(major = 8 AND minor IN (0, 1))
OR
(major BETWEEN 4 AND 7)
)
Copy
Windows assets
WITH WINDOWS AS (
SELECT DISTINCT name, version, install_location AS source
FROM programs
WHERE publisher = 'The PHP Group'
OR (name LIKE 'XAMPP%' AND publisher IN ('Bitnami', 'Apache Friends'))
UNION ALL
SELECT DISTINCT path AS name, product_version AS version, 'php.exe' AS source
FROM file
WHERE path LIKE 'C:\php%\php.exe'
OR path = 'C:\Program Files\PHP\php.exe'
OR path LIKE 'C:\Program Files\PHP\%\php.exe'
OR path = 'C:\Program Files (x86)\PHP\php.exe'
OR path LIKE 'C:\Program Files (x86)\PHP\%\php.exe'
OR path = 'C:\xampp\php\php.exe'
OR path LIKE 'C:\wamp%\bin\php\%\php.exe'
OR path LIKE 'C:\laragon\bin\php\%\php.exe'
OR path LIKE 'C:\tools\php%\php.exe'
OR path = 'C:\ProgramData\chocolatey\lib\php\tools\php.exe'
OR path LIKE 'C:\Users\%\scoop\apps\php\current\php.exe'
),
PARSED AS (
SELECT name, source, version,
CAST(REGEX_MATCH(version, '([0-9]+)\.([0-9]+)\.([0-9]+)', 1) AS INTEGER) AS major,
CAST(REGEX_MATCH(version, '([0-9]+)\.([0-9]+)\.([0-9]+)', 2) AS INTEGER) AS minor,
CAST(REGEX_MATCH(version, '([0-9]+)\.([0-9]+)\.([0-9]+)', 3) AS INTEGER) AS patch
FROM WINDOWS
)
SELECT name, source, version
FROM PARSED
WHERE
(major = 8 AND minor = 5 AND patch < 12)
OR
(major = 8 AND minor = 4 AND patch < 27)
OR
(major = 8 AND minor = 3 AND patch < 36)
OR
(major = 8 AND minor = 2 AND patch < 35)
OR
(major = 8 AND minor IN (0, 1))
OR
(major BETWEEN 4 AND 7)
Copy
Bottom line
The two critical vulnerabilities known as PHP2Shell (CVE-2026-109618 and CVE-2026-109619) allow attackers to achieve RCE on servers running PHP 5 and above. Make sure you patch PHP immediately.
Stay tuned
The Akamai Security Intelligence Group will continue to monitor, report on, and create mitigations for threats such as these for both our customers and the security community at large. To keep up with more breaking news from the Akamai Security Intelligence Group, check out ourresearch home page and follow us onsocial media.
Akamai SIG is a global team of world-class researchers, engineers, strategists, and data scientists with a broad range of expertise and security disciplines. Our data sources include the enormous Akamai Cloud, open sources, collaboration with third parties, and dark web intelligence. We have also developed our own algorithms and tools that help us deliver our research and keep Akamai security solutions up to date.
Read about CVE-2026-21589, a critical arbitrary file read vulnerability affecting multiple Atlassian Data Center products. Akamai customers are protected.
Learn how Akamai’s ExAR capabilities helped contain a complex supply chain intrusion targeting Linux, OpenShift, and Windows DMZ workloads in Latin America.