PHP's October 21, 2026 Security Updates: What to Patch and Verify
PHP released security updates on October 21, 2026 for two vulnerabilities that can allow unauthenticated remote code execution. Update the runtime, not just application dependencies. PHP’s warning covers web-serving installations including mod_php, PHP-FPM, CGI, FrankenPHP, and LiteSpeed. PHP’s release advisory.
This article reflects the advisories checked on October 23, 2026. It explains the confirmed conditions without including exploit payloads, then provides a small offline version-comparison exercise and a deployment checklist.
Affected and fixed versions
Both vulnerabilities have a CVSS v4.0 score of 9.2, Critical. The PHP-assigned CVE records were published on October 21:
The records give these boundaries for supported upstream branches:
| PHP branch | Affected upstream releases | First fixed release |
|---|---|---|
| 8.2 | 8.2.x before 8.2.35 | 8.2.35 |
| 8.3 | 8.3.x before 8.3.36 | 8.3.36 |
| 8.4 | 8.4.x before 8.4.27 | 8.4.27 |
| 8.5 | 8.5.x before 8.5.12 | 8.5.12 |
Do not interpret an omitted older branch as safe. PHP 8.1 and earlier are outside upstream support. PHP supplied exceptional minimal source backports for 7.4, 8.0, and 8.1, but these do not restore normal maintenance. Distribution and LTS packages may carry fixes under different package versions; obtain your provider’s advisory for both CVEs. PHP’s upgrade guidance explains these alternatives.
PHP 8.2’s security support ends on December 31, 2026. Apply the immediate fix and separately plan a supported-branch migration rather than treating this patch as a long-term upgrade plan. PHP support schedule.
What the two bugs actually require
CVE-2026-109618: failure during memory initialization
An out-of-memory failure can interrupt an internal operation before request state is fully initialized. Cleanup may then consume invalid state. PHP confirmed remote code execution against Apache mod_php, including during request startup before application code runs. The resulting code executes with the web worker’s privileges, not automatically as root.
The advisory distinguishes confirmed exploitation from susceptibility: FPM terminates a worker after failed startup, but application-time memory exhaustion remains a concern. A complete FPM exploit for this particular CVE was not demonstrated in the reviewed advisory. Do not confuse that qualification with immunity. PHP’s OOM advisory.
CVE-2026-109619: state surviving between requests
Here, worker-global state can retain a pointer into a finished request’s freed memory. Exploitation needs both an operation that leaves this state behind and a later operation that consumes it before initialization, with both requests reaching the same worker.
Confirmed cases include PHP 8.5 fatal-error backtraces on mod_php and session-upload-progress lifecycle paths on mod_php and FPM. The validated FPM session path depends on a suitable application operation after successful startup. Upload progress is enabled by default and does not require an application call to session_start().
These conditions explain why the broad update recommendation is stronger than a claim that every deployment has the same demonstrated exploit. PHP’s stale-state advisory.
Compare upstream versions without attacking a server
The following Python helper compares supplied version strings, not live servers. It sends no requests, changes no PHP settings, and does not prove a vendor backport or running process is patched. It deliberately refuses distribution suffixes, prereleases, and branches outside the four-row table.
The examples and displayed output were tested on Linux with Python 3.12.14. You need Bash, Python 3.12 or newer, a text editor, and the standard mktemp, rm, and rmdir utilities. No extra packages are required.
Create an isolated directory and keep using the same terminal:
start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1Save this as check_php_versions.py:
import re
import sys
FIXED = {(8, 2): 35, (8, 3): 36, (8, 4): 27, (8, 5): 12}
def classify(value):
if not re.fullmatch(r"[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}", value):
return 2, "REVIEW: use the package/vendor advisory"
major, minor, patch = map(int, value.split("."))
minimum = FIXED.get((major, minor))
if minimum is None:
return 2, "REVIEW: branch is outside this advisory table"
target = f"{major}.{minor}.{minimum}"
if patch < minimum:
return 1, f"BELOW upstream fix {target}; check vendor backports"
return 0, f"AT/ABOVE upstream fix {target}; verify deployed build"
def main(values):
if not values:
print("usage: python3 check_php_versions.py VERSION [VERSION ...]",
file=sys.stderr)
return 2
status = 0
for value in values:
code, message = classify(value)
print(f"{value}: {message}")
status = max(status, code)
return status
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))Compare an older release with all four fixed releases
The nonzero status is intentional: one input is below its upstream boundary. Running inside if also makes this demonstration safe in shells using set -e.
if python3 check_php_versions.py 8.2.34 8.2.35 8.3.36 8.4.27 8.5.12; then
status=0
else
status=$?
fi
printf 'exit status: %s\n' "$status"Output:
8.2.34: BELOW upstream fix 8.2.35; check vendor backports
8.2.35: AT/ABOVE upstream fix 8.2.35; verify deployed build
8.3.36: AT/ABOVE upstream fix 8.3.36; verify deployed build
8.4.27: AT/ABOVE upstream fix 8.4.27; verify deployed build
8.5.12: AT/ABOVE upstream fix 8.5.12; verify deployed build
exit status: 1Keep ambiguous versions out of the passing result
The -vendor1 suffix below is fictional test data, not a real distribution’s package identifier. An unknown future branch must also be reviewed rather than silently passing.
if python3 check_php_versions.py 8.1.33 8.4.26-vendor1 8.6.0RC3 8.6.0; then
status=0
else
status=$?
fi
printf 'exit status: %s\n' "$status"Output:
8.1.33: REVIEW: branch is outside this advisory table
8.4.26-vendor1: REVIEW: use the package/vendor advisory
8.6.0RC3: REVIEW: use the package/vendor advisory
8.6.0: REVIEW: branch is outside this advisory table
exit status: 2Exit status 0 means every supplied version reached its listed upstream boundary. Status 1 means at least one was below it; status 2 means at least one needs manual review, or no arguments were supplied. Status 2 takes precedence when results are mixed. None of these statuses assesses whether an attacker previously compromised a system.
Remove only the exercise file and its now-empty directory:
rm -- check_php_versions.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"Upgrade the serving runtime, then verify it
Use a change record for each deployment, with these checks:
- Inventory the actual request path. Record the host or image, PHP branch, full package/build identifier, SAPI, enabled extensions, and responsible service. Include every FPM pool, virtual host, replica, and older runtime still accepting traffic.
- Resolve the package evidence. Choose a fixed upstream release or a vendor package explicitly fixing both CVEs. Preserve the advisory link and exact package revision. A version comparison alone cannot establish a backport.
- Test the replacement. In staging, exercise normal login, sessions, uploads, imports, background jobs, and application error handling. Use ordinary requests, not an exploit to see whether production crashes.
- Roll out and replace old workers. Follow the service or platform’s supported restart/redeployment procedure. Confirm all replicas use the intended image or package, and that pre-update processes have exited. Keep any vulnerable rollback artifact out of the serving pool.
- Check the web runtime privately. Use an existing authenticated diagnostic or deployment instrumentation to verify the version, SAPI, and effective configuration serving requests. Do not leave a public
phpinfo()page behind. A shell’s PHP executable and configuration need not match the web service; PHP supports SAPI-specific configuration files. Configuration-file behavior. - Watch normal traffic after the change. Check application failures, worker restarts, session behavior, and upload errors. Retain relevant logs if compromise is suspected; patching is not an incident-cleanup procedure.
If an immediate upgrade is blocked
Treat temporary configuration changes as exposure reduction, not a passing patch check. For the OOM issue, PHP describes administratively enforced memory_limit=-1 alongside conservative request limits. Removing PHP’s memory ceiling increases resource-exhaustion risk, and underlying allocation failures remain possible. A cgroup/container boundary is not a complete fix; RLIMIT_AS and RLIMIT_DATA can trigger the vulnerable allocation-failure path. Read the full OOM mitigation guidance before considering this tradeoff.
For stale state, disabling session.upload_progress.enabled and, on PHP 8.5, fatal_error_backtraces blocks specific confirmed paths only. Apply these before request parsing and restart all workers; application ini_set() is too late. Hiding warnings does not eliminate the underlying problem. PHP’s configuration caveats matter here.
The useful completion criterion is concrete: every serving runtime is accounted for, both fixes are present, old workers are gone, and application checks pass. A quiet log or a successful version-helper run alone is not enough.
This post was created with assistance from AI.