On October 21, 2026 PHP fixed two critical flaws (CVSS 9.2) that allow unauthenticated code execution; the only complete mitigation is updating to 8.5.12, 8.4.27, 8.3.36 or 8.2.35, and anyone on 8.1 or earlier must ask their vendor for a patch or migrate.
1. What happened#
On October 20, 2026 the PHP project warned of an urgent security release for the next day. On October 21 it published versions 8.5.12, 8.4.27, 8.3.36 and 8.2.35, which fix two critical vulnerabilities allowing unauthenticated remote code execution. The press calls them PHP2Shell.
| Date (2026) | Event |
|---|---|
| October 20 | PHP announces a security release for October 21 at 14:00 UTC with two critical flaws (CVSS 9.2), and reserves the right to release earlier if details become public |
| Before the patches | Cloudflare's CTO said it was already seeing early attacks against both flaws; its WAF rules were temporary |
| October 21 | PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 ship, with the advisories published in PHP's repository |
| October 21 to 22 | Qualys reports that within 12 hours only 199,000 of 155 million installations with a known version were patched |
There are signs of exploitation: Cloudflare said it already saw early attacks against both flaws before the patches, and PHP describes a demonstrated exploit on Apache with mod_php. The UK's National Cyber Security Centre urged organizations to update immediately or take affected systems offline.
The sources differ on one detail: the advisories and PHP's news page date the releases October 21, while php.net's changelog shows the 22nd for 8.5.12 and 8.4.27. We use the 21st, the date of the advisories.
Documentation: PHP: 2026 news archive ↗
2. The two vulnerabilities#
Both have a CVSS v4.0 score of 9.2 (critical). According to BleepingComputer, they were discovered by Calif, OpenAI and Wiz. They share the same underlying problem: PHP leaves corrupt memory when a request ends abnormally.
| Item | CVE-2026-109618 | CVE-2026-109619 |
|---|---|---|
| Advisory | GHSA-w8hw-w9fx-j79w | GHSA-6wmf-4rv5-p3rp |
| Advisory title | RCE via OOM corruption | RCE via stale global state |
| CVSS v4.0 | 9.2 critical (AV:N/AC:H/AT:P/PR:N/UI:N) | 9.2 critical (AV:N/AC:H/AT:P/PR:N/UI:N) |
| Mechanism | PHP leaves an out-of-memory error with longjmp() and leaves request data half-initialized that shutdown treats as valid | Global state from one request survives its freed memory and a later request on the same worker uses it before it is reset |
| Confirmed impact | Unauthenticated remote code execution with the web worker's privileges | Use-after-free, memory disclosure and unauthenticated remote code execution |
| Where it was confirmed | Apache mod_php with default limits | mod_php (PHP 8.5 fatal backtraces) and mod_php and PHP-FPM (session upload progress) |
The first flaw triggers when memory runs out. The advisories describe several ways to push a request into that state while staying within apparent limits: multipart requests with no usable declared length, unlimited headers per part and empty file fields with nested names. The second needs two requests handled by the same worker: one that leaves the state stale and one that uses it.
The advisory clarifies that exploit success depends on the PHP build, the extensions and the memory layout, so it is harder in some environments than in others. That does not make it safe: researchers demonstrated code execution with default limits.
Documentation: Advisory GHSA-w8hw-w9fx-j79w (CVE-2026-109618) ↗ · Advisory GHSA-6wmf-4rv5-p3rp (CVE-2026-109619) ↗
3. Who is affected#
All versions below 8.2.35, 8.3.36, 8.4.27 and 8.5.12 are affected. Qualys and the press include in the risk any installation serving web requests: PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed.
| PHP branch | Fixed version | Status |
|---|---|---|
| 8.5 | 8.5.12 | Supported |
| 8.4 | 8.4.27 | Supported |
| 8.3 | 8.3.36 | Supported |
| 8.2 | 8.2.35 | Reaches end of life on December 31, 2026 |
| 8.1, 8.0 and 7.4 | No official fixed version | Out of support; PHP offers source patches for 7.4, 8.0 and 8.1 |
The most urgent case is Apache with mod_php: the advisory describes an exploit that fires before application code runs. In PHP-FPM the first flaw is not fully demonstrated, but PHP validated code execution through the session path. The command line is not mentioned.
Branches 8.1, 8.0 and 7.4 get no updates from the project; PHP offers source patches that your vendor must apply. Qualys estimates that of the 167 million PHP installations it analyzed, 71% use unsupported versions (from 5.4 to 8.1). It is a security vendor's figure: use it as an order of magnitude.
Documentation: Qualys: PHP2Shell, how to find, mitigate and patch ↗ · BleepingComputer: emergency patches for PHP2Shell ↗
4. How to check if you are affected#
Look at the version that serves web requests, not the one you see in your terminal: Qualys warns that php -v may show a different installation from the one serving the site. Also check containers, parallel branches and the PHP bundled with hosting panels.
# The version that serves web requests (not the terminal's): compare it with the running process
php -v
php-fpm -v || php-fpm8.4 -v
apache2ctl -M 2>/dev/null | grep -i php # is mod_php loaded?
ps aux | grep -E "php-fpm|apache2|httpd|frankenphp" | grep -v grep
# Containers: the image that runs, not the one on your laptop
docker ps --format "{{.Names}} {{.Image}}"
docker exec NAME php -v
# After updating: really restart the processes that serve requests
sudo systemctl restart php8.4-fpm # or apache2 if you use mod_phpThe script compares a version with the fixed ones. If you use a distribution package, the number may still be old even though it has the patch: check your vendor's advisory.
import re
FIXED = {(8, 5): 12, (8, 4): 27, (8, 3): 36, (8, 2): 35} # fixed patch level per branch
def parse(version):
return tuple(int(n) for n in re.findall(r"\d+", version)[:3])
def status(version):
major, minor, patch = parse(version)
if (major, minor) in FIXED:
minimum = FIXED[(major, minor)]
if patch >= minimum:
return "fixed"
return f"VULNERABLE: update to {major}.{minor}.{minimum}"
if (major, minor) > (8, 5):
return "branch newer than the fixed ones: check the advisories"
return "NO OFFICIAL FIXED VERSION: ask your vendor for a build with the patch or migrate"
for v in ["8.5.11", "8.5.12", "8.4.26", "8.4.27", "8.3.36", "8.2.34", "8.1.31", "7.4.33"]:
print(v, "->", status(v))5. How to patch#
- Update to 8.5.12, 8.4.27, 8.3.36 or 8.2.35 according to your branch. If you are on 8.1 or earlier, ask your vendor for a build with the patch or migrate to a supported branch.
- Restart the processes that serve requests (PHP-FPM, Apache, FrankenPHP): updating the package is not enough until the workers load the new version.
- In containers, rebuild the image on a base with the fixed version and redeploy.
- On shared hosting or with a panel (Plesk, cPanel), confirm with the provider that it has applied the patch.
- If PHP 8.2 is your branch, plan the migration: it reaches end of life on December 31, 2026.
Go deeperOctober 2026 security patches: Next.js 16.4.1 and PgBouncer 1.26.1
6. If you cannot patch today: temporary mitigations#
PHP publishes settings that reduce exposure, but insists they do not make an unpatched system safe: the only complete mitigation is updating. They must be applied administratively (php.ini, the FPM pool or the server configuration), not with ini_set() or .user.ini, and workers must be restarted.
| Setting | Value | Protects against |
|---|---|---|
| memory_limit | -1, set with php_admin_value | CVE-2026-109618 |
| post_max_size | 8M without uploads; 32M with uploads | CVE-2026-109618 |
| upload_max_filesize | Lower than post_max_size | CVE-2026-109618 |
| session.upload_progress.enabled | 0 in php.ini, the FPM pool or the server; not with ini_set() or .user.ini | CVE-2026-109619 |
| fatal_error_backtraces | 0, PHP 8.5 only | CVE-2026-109619 (the confirmed PHP 8.5 path) |
| pm.max_requests | 1, FPM only and with a performance cost | CVE-2026-109619 |
import re
def to_mb(value):
m = re.fullmatch(r"(\d+)\s*([KMG]?)", value.strip().upper())
if not m:
return None
return int(m.group(1)) * {"": 1 / 1024**2, "K": 1 / 1024, "M": 1, "G": 1024}[m.group(2)]
def audit(ini, version):
"""Checks the temporary mitigations PHP lists; they do not replace the patch."""
cfg = dict(l.split("=", 1) for l in (x.split(";")[0].strip() for x in ini.splitlines()) if "=" in l)
cfg = {k.strip(): v.strip() for k, v in cfg.items()}
missing = []
if cfg.get("memory_limit") != "-1":
missing.append("memory_limit = -1 (set as php_admin_value)")
limit = to_mb(cfg.get("post_max_size", "8M"))
if limit is None or limit > 32:
missing.append("post_max_size <= 32M (8M if there are no uploads)")
if cfg.get("session.upload_progress.enabled", "1").lower() not in ("0", "off"):
missing.append("session.upload_progress.enabled = 0")
if parse(version)[:2] == (8, 5) and cfg.get("fatal_error_backtraces", "1").lower() not in ("0", "off"):
missing.append("fatal_error_backtraces = 0 (PHP 8.5 only)")
return missing
def parse(version):
return tuple(int(n) for n in re.findall(r"\d+", version)[:3])
partial = "memory_limit = -1\npost_max_size = 8M ; no uploads\nsession.upload_progress.enabled = 0\n"
print(audit(partial, "8.4.26"))
print(audit(partial, "8.5.11"))
print(audit("memory_limit = 128M\npost_max_size = 64M\n", "8.3.35"))Two warnings from the advisory: do not use RLIMIT_AS or RLIMIT_DATA process limits, because they can trigger the vulnerable path, and hiding errors with error_reporting, display_errors or the @ operator does not help. Also, memory_limit = -1 only works if no application can set a finite limit again.
7. After patching#
Qualys reports that researchers demonstrated unauthenticated disclosure of worker memory, which can expose passwords, tokens and API keys. If your server was exposed with a vulnerable version, treat the secrets that passed through PHP as possibly compromised.
- Rotate the database credentials, API keys and session tokens that application handled.
- Review the server and PHP logs since October 20: odd or very large multipart requests, repeated fatal errors and worker restarts.
- Look for new or modified files in the web directory and in upload folders.
- Keep a WAF rule that limits or blocks multipart where you do not use it, as an extra measure, not a substitute for the patch.
- Record the date and version at which you patched each server.
These recommendations are good practice, not from PHP's advisory; adapt them to your environment and your security team.
8. Keep your numbers current#
The versions, identifiers and mitigations are those of the advisories published through October 22, 2026. New advisories and exploitation details may appear; check PHP's security repository and your distribution's advisory.
- Subscribe to the security advisories of PHP and your vendor.
- Keep an inventory of which PHP version runs on each server and container.
- Automate an alert when a version falls below the fixed one.
Sources and scope
Documentation checked on October 22, 2026. Examples and decision criteria are editorial proposals; adapt them to your application's contract and validate them in an authorized test environment.