🚨 TL;DR
Two critical, unauthenticated PHP remote-code-execution bugs (CVE-2026-109618 and CVE-2026-109619, both CVSS v4.0 9.2) were disclosed on Oct 21, 2026.
Update to 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12.
composer updatewill not fix this. It's the PHP runtime, not your dependencies.
PHP's own words: "updating is the only safe choice."
What is PHP2Shell?
It's the nickname for two bugs in PHP's core engine. Both can give an attacker code execution as your web worker user, with no login.
| CVE | In plain English |
|---|---|
| CVE-2026-109618 | PHP runs out of memory mid-request and leaves internal data half-built. Cleanup then trusts that broken data, causing memory corruption. |
| CVE-2026-109619 | Stale state from an earlier request is left behind in the worker. A later request can end up using it. |
Laravel, WordPress and Symfony are not the bug. They just run on the vulnerable runtime.
🧠 How did this happen? (It's a memory story)
PHP's engine is written in C, and C makes the developer manage memory by hand. Both bugs are classic memory-safety mistakes, where PHP's internal bookkeeping gets out of sync with what is actually in memory.
Bug 1: running out of memory at the worst moment (CVE-2026-109618)
Think of PHP building a request like assembling furniture from a box. If memory runs out halfway through, PHP has to stop and clean up. Here, it stops while the internal request data is only partly built. The cleanup code then assumes everything is complete and valid, and works on broken data. That leads to memory corruption, which an attacker can steer into running their own code.
Bug 2: stale state left behind (CVE-2026-109619)
A PHP worker handles many requests over its life. Some internal state belongs to one request, but something still points at it after it should have been reset. A later request on the same worker can touch that stale state. Imagine returning a hotel key card and someone else still using the old room number.
Why it's so serious: these bugs live in PHP's core request handling, so they happen before or around your application code. That's why updating your framework doesn't help, and why the PHP team calls patching the only safe fix.
Per the advisory's mitigation list, known ways to reach the bugs involve hitting memory limits, large or unusual uploads, and diagnostics or fatal errors that fire before PHP finishes initializing a request. That's also why the stopgaps below focus on memory limits, upload sizes and error paths.
Am I affected?
If PHP serves web requests for you, yes, until you patch. That covers PHP-FPM, Apache mod_php, php-cgi, FrankenPHP and LiteSpeed.
According to PHP's advisory, mod_php is the highest-risk setup: an unauthenticated request can trigger the bug before any of your application code runs. So login pages, auth middleware and framework security won't help there.
Fixed versions
| Branch | Patched release |
|---|---|
| 8.2 | 8.2.35 |
| 8.3 | 8.3.36 |
| 8.4 | 8.4.27 |
| 8.5 | 8.5.12 |
Older than 8.2?
- 8.1 and below get no official releases. Ask your distro or LTS vendor for a backport.
- Building from source? PHP published minimal source backports for 7.4, 8.0 and 8.1 in
php/php-src(the advisory lists the branch names). - Heads up: AWS notes that on end-of-life runtimes there is no fix for CVE-2026-109618 beyond these backports and vendor LTS builds, so treat migration to a supported branch as urgent.
- 8.2 hits end of life on Dec 31, 2026. Patch now, then plan your upgrade.
⚡ Check everything in 60 seconds
php -v only shows your CLI PHP, and your website may run on a different one. This script checks the CLI, any FPM binaries and your running Docker containers:
#!/usr/bin/env bash
# php2shell-check.sh: compares PHP versions against the fixed releases
fixed() { case "$1" in 8.2) echo 35;; 8.3) echo 36;; 8.4) echo 27;; 8.5) echo 12;; esac; }
report() {
label="$1"; ver="$2"
[ -z "$ver" ] && return
branch="${ver%.*}"; patch="${ver##*.}"; need="$(fixed "$branch")"
if [ -z "$need" ]; then
echo "⚠️ $label: PHP $ver (unsupported branch, ask your vendor about backports)"
elif [ "$patch" -ge "$need" ] 2>/dev/null; then
echo "✅ $label: PHP $ver"
else
echo "❌ $label: PHP $ver (need $branch.$need or newer)"
fi
}
# 1) CLI
if command -v php >/dev/null; then
report "CLI" "$(php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION.".".PHP_RELEASE_VERSION;')"
fi
# 2) FPM binaries
for b in /usr/sbin/php-fpm* /usr/local/sbin/php-fpm*; do
[ -x "$b" ] || continue
report "FPM ($b)" "$("$b" -v 2>/dev/null | grep -oE 'PHP [0-9]+\.[0-9]+\.[0-9]+' | head -1 | cut -d' ' -f2)"
done
# 3) Apache mod_php
if command -v apachectl >/dev/null && apachectl -M 2>/dev/null | grep -qi php; then
echo "⚠️ Apache has a PHP module loaded (mod_php, highest risk). Check its version."
fi
# 4) Docker containers
if command -v docker >/dev/null; then
for c in $(docker ps --format '{{.Names}}'); do
report "Docker: $c" "$(docker exec "$c" php -r 'echo PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION.".".PHP_RELEASE_VERSION;' 2>/dev/null)"
done
fi
Save it, run bash php2shell-check.sh, and you're done. The script only reads version numbers; it changes nothing.
One catch: this script compares upstream version numbers. Debian, Ubuntu, RHEL and others sometimes backport fixes without changing the version, so a ❌ can be a false alarm. Check your vendor's security notice before you panic.
🛠️ Patch it
Debian/Ubuntu example (package and service names depend on your PHP version):
sudo apt update && sudo apt install --only-upgrade php8.3-fpm
sudo systemctl restart php8.3-fpm
Check whether your distro has shipped the fix:
apt changelog php8.3-fpm | grep -i "CVE-2026-10961"
Some vendors (RHEL-family builds, for example) only write "rebase to 8.3.36" in the changelog and never name the CVE, so an empty grep does not prove you're unpatched. Confirm the installed version is 8.3.36 or newer as well.
Docker: switch to a patched base image and rebuild with no cache. Confirm the patched tag exists on Docker Hub first.
# Use a patched tag: 8.3.36 or newer
FROM php:8.3-fpm
docker build --pull --no-cache -t myapp .
docker compose up -d --force-recreate
Then rescan your images with Trivy, Grype or whatever you use.
Shared or managed hosting: switch to a patched PHP version in your control panel, or ask your host to confirm.
⏳ Can't patch today? Stopgaps (NOT a fix)
PHP is clear: these reduce exposure to specific attack paths but don't make you safe. See the official advisory for details.
CVE-2026-109618 (out of memory):
- Set
memory_limit=-1where apps can't override it (for examplephp_admin_valuein your FPM pool or Apache config). Make sure nothing puts a finite limit back. A real allocation failure can still reach the bug. - Lower
post_max_size: 8M if you don't need uploads, 32M if you do. Keepupload_max_filesizebelow it. - Cap request size, field count, multipart parts, header count and nesting depth at your web server or proxy.
- Use container/cgroup limits as an availability boundary. Don't use
RLIMIT_ASorRLIMIT_DATA, because they can trigger the vulnerable failure.
CVE-2026-109619 (stale state):
- Set
session.upload_progress.enabled=0inphp.ini, your FPM pool or server config, then restart every worker.ini_set()and.user.inidon't count. Note that this blocks the confirmed attack paths, not necessarily every variant. -
PHP 8.5 only: set
fatal_error_backtraces=0and restart workers. - Fix attacker-reachable fatal errors and timeouts, especially in upload and import code.
Add a WAF layer. Several WAF and security vendors have published rules for known exploit patterns. It helps, but it doesn't replace patching.
🆕 Update, Oct 23: one more setting to flip
Two days after the release, PHP published an informational advisory. It recommends keeping session.upload_progress.enabled=0 until the next regular bugfix release, even if you already patched. It covers an unlikely RCE vector tracked as CVE-2026-115560. The ID is already assigned, and PHP says the full advisory will be published with the next release.
It only matters if all three are true:
-
session.serialize_handleris set tophp_binaryorphp_serialize - Your code switches it to
phpwithini_set()beforesession_start() - Session upload progress is still enabled
Never switch serialize handlers at runtime? You can leave it on. Not sure? Turn it off, since most apps never use it.
✅ After patching: verify
- [ ] Restarted or reloaded every worker (FPM pools, Apache, containers)
- [ ] Confirmed the web runtime version, not just CLI
- [ ] Checked staging, queue workers, cron containers and internal tools
- [ ] Smoke-tested login, DB, queues and scheduled jobs
🔍 Were you already hit?
Security researchers (watchTowr) report that both bugs are being exploited in the wild, so treat this as urgent. If you ran a vulnerable version while exposed:
- Look in web, PHP and system logs for odd errors, strange processes and modified files
- Check web-writable directories for files you don't recognize
- If you find real evidence: isolate the host, preserve evidence, rotate secrets (
APP_KEY, DB passwords, API keys, mail credentials) and rebuild from a trusted source
Clean logs don't prove you're safe, and patching doesn't kick out someone who's already inside.
❓ Myths
"I updated Laravel/WordPress, so I'm fine." No. The bug is in the PHP runtime. composer update and composer audit don't touch it.
"My app requires login." On mod_php, the attack can fire before your app code runs, so app-level auth doesn't help.
"I'm behind Cloudflare." A WAF reduces risk for known exploit patterns. Patching is the real fix.
"php -v shows a patched version." That's your CLI. Check what actually serves web traffic.
📬 Bonus for devs: retest your email flows with InboxOro
Patching and rotating secrets often means touching mail credentials, APP_KEY or environment config. That can quietly break email-dependent features, so retest them before users find out:
- Signup and email verification links
- OTP and 2FA codes
- Password-reset emails
- Welcome and notification emails
I built InboxOro, a temporary email platform for privacy-conscious users and developers, for exactly this kind of testing. Instead of burning through your personal address or creating throwaway Gmail accounts, you grab a temporary inbox, trigger the email from your app, and check that the message and link arrive as expected.
A quick post-patch routine:
- Open a temporary inbox on InboxOro
- Register a test account in your app using that address
- Confirm the verification email arrives and the link works
- Trigger a password reset and check that email too
To be clear, InboxOro doesn't protect against PHP2Shell. Only patching your PHP runtime does. It just helps you confirm your app's email flows still work afterwards. Give it a try at inboxoro.com, and I'd love your feedback in the comments.
📚 Sources
- Official PHP security advisory (Oct 21, 2026)
- PHP informational advisory: session upload progress (Oct 23, 2026)
- CVE-2026-109618 (GHSA-w8hw-w9fx-j79w)
- CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp)
- CERT-EU Security Advisory 2026-018
Always verify guidance for your own distro and hosting environment before changing production.
Did the script flag anything on your servers? Tell me in the comments. 👇

Top comments (0)