PHP shipped the PHP2Shell fixes on October 21: 8.5.12, 8.4.27, 8.3.36 and 8.2.35. You patched that week, and php -v says you’re done.
On October 26, CISA added both CVEs to its Known Exploited Vulnerabilities catalog. CISA lists a bug there only when it has evidence of exploitation in the wild.
php -v describes the binary on disk. It says nothing about the process that answers your requests.
And both bugs live in that process.
CVE-2026-109618: an out-of-memory error can interrupt PHP while request data is half built, and request cleanup then treats it as valid.
CVE-2026-109619: PHP frees memory from one request while it still points at it. A later request on the same worker can use that stale pointer.
The 109618 advisory states the impact plainly: “unauthenticated remote code execution as the web worker account”. PHP scores both bugs 9.2. NVD scores 109618 at 9.8 and 109619 at 8.1.
php.net names FPM, mod_php, php-cgi, FrankenPHP and LiteSpeed as at risk. It rates mod_php highest, because there “an unauthenticated request can trigger the attack before any application code runs”.
The advisories don’t mention Swoole or RoadRunner, whose workers get requests already parsed by Swoole or by Go. Swoole’s maintainers say neither CVE affects Swoole (swoole-src#6321). RoadRunner hasn’t said. Those workers still run the unfixed engine, so patch and restart them like everything else.
CISA’s catalog entry names no victims. Palo Alto Networks lists PAN-OS as affected and reports active exploitation against its management interface where it’s exposed to the internet.
Laravel’s post-mortem of its own response says: “We found no evidence that any customer was exploited as a result of these vulnerabilities.”
I tested the restarts Laravel developers actually run. octane:reload on RoadRunner, Swoole and FrankenPHP. Herd on my Mac. A Linux FPM pool after a package upgrade. Ubuntu’s own packages, including the PHP 8.1 backport.
I ran them on October 22, and ran the FrankenPHP, FPM and /proc checks on November 10.
On two of the three Octane servers, the reload Forge tells you to run left the unfixed engine serving requests.
And the FPM check in Forge’s guide reads the file on disk, not the pool that serves your site.
Ask the worker, not the binary
Mattias Geniar found the fingerprint. The fix registers a new INI directive, memory_hard_limit_percent (main/main.c:828 at the 8.4.27 tag).
ini_get() returns false for a directive the build doesn’t know. So an unfixed 8.2+ build returns false, and a fixed one returns an empty string, which is the unset default. 8.1 is different; see Trap 4.
He runs it from the CLI.
Your app isn’t served by your terminal’s PHP. An FPM pool, an Apache child or an Octane worker serves it, and each one keeps the engine it loaded when it started.
So ask the process that serves requests. Add a signed route (Laravel 10 and later):
// routes/web.php
use Illuminate\Support\Facades\Route;
Route::get('/php-check', fn () => [
'version' => PHP_VERSION,
'sapi' => PHP_SAPI,
'php2shell_fix' => ini_get('memory_hard_limit_percent') !== false,
'pid' => getmypid(),
])->name('php-check')->middleware('signed:relative');
Generate a URL on the server, then call it. This is Octane on Ubuntu’s unfixed PHP:
$ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:8000/php-check # no signature
403
$ url=$(php artisan tinker --execute="echo URL::temporarySignedRoute('php-check', now()->addMinutes(10), absolute: false);")
$ echo "$url"
/php-check?expires=1792650040&signature=<64 hex chars>
$ curl -s "http://127.0.0.1:8000$url"
{"version":"8.3.6","sapi":"cli","php2shell_fix":false,"pid":173}
$ curl -s "http://127.0.0.1:8000$url" # again: which worker answers
{"version":"8.3.6","sapi":"cli","php2shell_fix":false,"pid":172}
"sapi":"cli" is right. Swoole and RoadRunner workers run under PHP’s CLI SAPI. The pid shows it’s a worker, not your shell.
Use signed, not auth. If your app has public signup, any account holder can read auth-protected output. That includes someone checking whether your servers are still open.
The relative signature leaves the host out of the hash. Behind nginx, call your site’s domain. Behind a load balancer, send the same URL to each server’s private address with your domain in the Host header:
curl -s -H "Host: example.com" "http://10.0.0.5$url"
Each answer covers only the process that sent it.
Ask each pool a few times and compare the pids, and do that for every PHP version and pool your sites use.
Some pools only get certain requests: only Xdebug requests reach Herd’s Xdebug pool in Trap 2, so a plain request never asks it. Check a pool like that through /proc, as Trap 2 does.
No deploy window? On an FPM server, a one-file version of the check goes straight to the pool’s socket. cgi-fcgi comes from the libfcgi-bin package:
<?php
// patch-check.php: run it through the SAPI you want to check.
$fixed = ini_get('memory_hard_limit_percent') !== false;
printf("PHP_VERSION %s\n", PHP_VERSION);
printf("PHP_SAPI %s\n", PHP_SAPI);
printf("hard limit INI %s\n", $fixed ? 'registered' : 'missing');
printf("upload_progress %s\n", ini_get('session.upload_progress.enabled') ?: '0');
if (PHP_VERSION_ID >= 80500) {
printf("fatal_backtraces %s\n", ini_get('fatal_error_backtraces') ?: '0');
}
echo $fixed ? "RESULT fixed\n" : "RESULT NOT fixed\n";
$ SCRIPT_FILENAME=/w/patch-check.php REQUEST_METHOD=GET cgi-fcgi -bind -connect 127.0.0.1:9000
X-Powered-By: PHP/8.4.27
Content-type: text/html; charset=UTF-8
PHP_VERSION 8.4.27
PHP_SAPI fpm-fcgi
hard limit INI registered
upload_progress 1
RESULT fixed
On Ubuntu and Forge, -connect takes the socket path instead, such as /run/php/php8.4-fpm.sock. I ran this file on the official Docker images: all four branches through the CLI, and 8.4 through FPM and Apache. Every unfixed tag said NOT fixed. Every fixed tag said fixed.
What about X-Powered-By? If your build reports upstream version numbers and you still send that header, curl -sI against your site tells you the serving version. It breaks on Ubuntu’s packages (Trap 3), and when expose_php is off.
Trap 1: octane:reload keeps the old engine on Swoole and FrankenPHP
Forge’s guide for this release still tells you to run queue:restart, horizon:terminate and octane:reload after updating PHP.
Laravel’s November 4 post-mortem adds no restart step. It says Forge customers had to “upgrade PHP on each of their servers”, and never mentions restarting a queue worker, Horizon or Octane.
Octane’s own docs say to reload “so that your newly deployed code is loaded into memory”. Code, not the engine.
Mattias Geniar said so on October 21: “octane:reload reloads your code, not PHP.” The next day, Safeguard and Daryl Legion explained why a reload keeps the old engine on Swoole, and Safeguard added FrankenPHP.
None of them published an Octane run, and Safeguard said only that RoadRunner’s workers “may pick up a new binary”.
I built a lab to see what a reload actually loads. Ubuntu 24.04 with its own PHP 8.3, pinned to the unfixed build. Laravel 13.38, Octane 2.22, Swoole 6.2.3, the RoadRunner 3.0.1 binary and the route above. The lab’s copy drops the signed middleware, so plain curl reaches it.
On RoadRunner and Swoole, “update PHP” meant installing Ubuntu’s security update. On FrankenPHP it meant replacing ./frankenphp v1.13.0, which embeds PHP 8.5.11, with v1.13.1, which embeds 8.5.12.
Swoole, after the update:
$ dpkg-query -W php8.3-cli
php8.3-cli 8.3.6-0ubuntu0.24.04.13
$ php -r 'var_dump(ini_get("memory_hard_limit_percent") !== false);' # the CLI
bool(true)
$ curl -s http://127.0.0.1:8000/php-check
{"version":"8.3.6","sapi":"cli","php2shell_fix":false,"pid":30}
$ php artisan octane:reload --server=swoole
INFO Reloading workers...
$ curl -s http://127.0.0.1:8000/php-check
{"version":"8.3.6","sapi":"cli","php2shell_fix":false,"pid":6803}
The CLI says fixed. The worker says it isn’t, before the reload and after it, even though the reload gave it a new PID. (The version stays 8.3.6 on both sides. Trap 3 explains why.)
RoadRunner, same update, same reload:
$ php artisan octane:reload --server=roadrunner
INFO Reloading workers...
$ curl -s http://127.0.0.1:8000/php-check
{"version":"8.3.6","sapi":"cli","php2shell_fix":true,"pid":6824}
| Server | After octane:reload | After octane:stop + octane:start |
|---|---|---|
| RoadRunner | fixed | fixed |
| Swoole | not fixed | fixed |
| FrankenPHP | not fixed | fixed |
The source explains all three.
The links point at Octane v2.22.0, the version the lab ran. v2.23.0 doesn’t touch these files.
Swoole’s reload sends SIGUSR1 to the master. The new workers fork from a master that loaded the old PHP.
FrankenPHP’s reload sends its own Caddy config back to itself, inside the same process. PID 51 before, PID 51 after.
RoadRunner’s reload runs rr reset. That starts fresh php processes from the binary on disk.
So Forge’s step is right for RoadRunner and wrong for the other two. On Swoole and FrankenPHP, stop the server and start it again. On Forge, restart the daemon that runs Octane, one server at a time behind your load balancer.
On Swoole, its maintainers say these two CVEs don’t apply. The reload behaves the same for every PHP security release, though, and FrankenPHP is on php.net’s at-risk list.
FrankenPHP has a second problem: it compiles its PHP into the frankenphp binary that Octane downloaded into your project. apt never touches it. Octane’s docs have said so since October 22: delete the binary, run php artisan octane:install --server=frankenphp, then restart Octane.
Octane v2.22.0, the version in my lab, only checked that the binary was at least 1.13.0. v2.23.0, released November 3, requires 1.13.2 (#1176). When octane:start finds an older binary, it offers to download the latest release, and under --no-interaction it downloads it.
octane:reload never runs that check.
And a binary outside your project, such as one on your PATH in a Docker image, only gets a warning (#1177).
The version number doesn’t tell you which PHP is inside anyway, because FrankenPHP rebuilds release assets after the release:
v1.13.0 release published 2026-10-04T14:01:03Z asset frankenphp-linux-aarch64 uploaded 2026-10-16T01:16:22Z
v1.13.1 release published 2026-10-16T21:55:55Z asset frankenphp-linux-aarch64 uploaded 2026-10-21T15:41:44Z
…
v1.13.0: FrankenPHP v1.13.0 PHP 8.5.11 Caddy v2.11.7 …
v1.13.1: FrankenPHP v1.13.1 PHP 8.5.12 Caddy v2.11.7 …
A v1.13.1 binary you downloaded before FrankenPHP rebuilt it on the afternoon of October 21 predates the fix. Run ./frankenphp version and read the PHP number, not the release.
If yours reports 8.5.12, it has the PHP2Shell fix but none of FrankenPHP’s own fixes since. Take the latest release instead.
v1.13.2, the release FrankenPHP built for the fix, shipped static binaries that crashed.
frankenphp#2743 reported the x86_64 build dying with SIGILL on cloud CPUs without SHA-NI, including AWS under Octane, sometimes seconds after it started. v1.13.3 fixed that.
In my arm64 VM, the v1.13.2 arm64 build, as downloaded on October 22, died with Illegal instruction.
And v1.13.4 fixed a worker-mode crash that affects only 1.13.2 and 1.13.3. Octane’s new 1.13.2 minimum lets both of those through.
The latest release, v1.13.5, ran without a crash in the same VM on November 10:
$ ./frankenphp version # as downloaded now
FrankenPHP v1.13.5 PHP 8.5.12 Caddy v2.11.9 h1:bhZtcRKQOq0fuLvEgCdPYvUxXyGn9kzjG4lh2332DTE=
$ curl -s http://127.0.0.1:8000/php-check
{"version":"8.5.12","sapi":"frankenphp","php2shell_fix":true,"pid":68}
…
$ pgrep -c frankenphp # still running after ~30 s
1
After the restart, call the route a few times over a minute. ./frankenphp version alone won’t catch a crash that comes seconds after start.
Trap 2: a process that started before the update
Herd ships its PHP binaries apart from the Herd app, in ~/Library/Application Support/Herd/bin. On my Mac, the 8.5 binaries changed on the evening of October 21. My next check caught this (I trimmed the ls and lsof columns with awk):
$ php85-fpm -v | head -1 # the binary on disk
PHP 8.5.12 (fpm-fcgi) (built: Oct 21 2026 14:42:55) (NTS clang 15.0.0)
$ ls -li php85-fpm
inode 34171158 size 93858832 modified Oct 21 23:22
$ ps -axo pid,lstart,command | grep "php-fpm: master" | grep 8.5
35630 Wed Oct 21 23:20:57 2026 php-fpm: master process (~/Library/Application Support/Herd/config/fpm/8.5-fpm-debug.conf)
38082 Wed Oct 21 23:22:33 2026 php-fpm: master process (~/Library/Application Support/Herd/config/fpm/8.5-fpm.conf)
$ lsof -p 35630 | awk '$4=="txt" && $NF ~ /php85-fpm$/' # the executable this master runs
pid 35630 size 93755248 inode 10640348
The normal 8.5 pool restarted after the swap.
The Xdebug pool had started 96 seconds earlier. It kept executing the old file: a different inode and a different size from the one php85-fpm -v reads. The route would never have shown it, because only Xdebug requests reach that pool.
After herd restart, both masters ran inode 34171158. Herd sites stay on your machine until you herd share one. Then the internet reaches its PHP.
On a Linux server, upgrading the php8.x-fpm package restarts FPM for you. Forge’s guide covers the case where it doesn’t: “If PHP-FPM did not restart automatically, you may restart it from your server’s dashboard”.
When the restart doesn’t happen, /proc shows it. Here the container blocked the package’s restart:
### 2. after apt-get install php8.3-* (the package could not restart FPM in this container)
invoke-rc.d: could not determine current runlevel
$ dpkg-query -W php8.3-fpm
php8.3-fpm 8.3.6-0ubuntu0.24.04.13
$ php-fpm8.3 -v | head -1 # the binary on disk
PHP 8.3.6 (fpm-fcgi) (built: Oct 19 2026 15:16:58)
$ SCRIPT_FILENAME=/w/patch-check.php REQUEST_METHOD=GET cgi-fcgi -bind -connect /run/php/php8.3-fpm.sock | tail -1
RESULT NOT fixed
$ readlink /proc/$(pgrep -o php-fpm8.3)/exe # the FPM master
/usr/sbin/php-fpm8.3 (deleted)
(deleted) means the master still runs a file the package replaced. After a restart (systemctl restart php8.3-fpm on a server), the same check said fixed.
The php-fpm8.3 -v line printed the new build, from October 19, while the master serving requests still ran the old file. (The build predates the release because distributions got the patches in advance.)
Forge’s guide tells you to run that exact check: “To confirm that PHP-FPM is also running the patched version, run: php-fpm8.4 -v”.
It confirms nothing about the running pool. php-fpm8.4 -v starts a new process from the file on disk, the same as php -v.
Upgrade every php8.x-* package, including FPM. Forge’s guide has a one-liner for it.
The processes no package restarts are yours: Octane, Horizon, queue workers, Reverb.
The route can’t reach most of them. /proc can.
This lists every process still running a PHP binary the package replaced. Here, a queue worker and Octane on Swoole after Ubuntu’s update:
$ find /proc -maxdepth 2 -name exe -lname '*php*(deleted)' -printf '%h %l\n' 2>/dev/null
/proc/23 /usr/bin/php8.3 (deleted)
/proc/29 /usr/bin/php8.3 (deleted)
/proc/41 /usr/bin/php8.3 (deleted)
…
$ ps -o pid,args -p "$(find /proc -maxdepth 2 -name exe -lname '*php*(deleted)' 2>/dev/null | cut -d/ -f3 | paste -sd,)"
PID COMMAND
23 php artisan queue:work --sleep=3
29 php artisan octane:start --server=swoole --host=127.0.0.1 --port=8000 --workers=2 --no-interaction
41 swoole_http_server: master process for Laravel
…
After queue:restart and a stop and start of Octane, the same find printed nothing.
On a server, run it with sudo: /proc won’t show you the executable of another user’s process (proc(5)), and 2>/dev/null hides the refusals.
Geniar’s patch guide does the same job with needrestart and a /proc maps scan.
(deleted) tells you a process runs a replaced file. It can’t tell you whether the file that replaced it carries the fix. For a process that serves HTTP on 8.2 and later, the route can.
I found no report that the bugs reach Horizon or queue workers, which don’t serve HTTP. They run the same PHP, though. Restart them anyway, as Forge’s guide says, until the find above prints nothing.
Trap 3: php -v prints 8.3.6 before and after
This one is for you if your server runs Ubuntu’s own PHP packages, not the ondrej PPA that Forge installs, and not a Docker image. Ubuntu backports the fix and keeps the upstream number for the life of the release:
### before: php8.3-cli=8.3.6-0maysync1
$ dpkg-query -W php8.3-cli
php8.3-cli 8.3.6-0maysync1
$ php -v | head -1
PHP 8.3.6 (cli) (built: Apr 15 2024 19:21:47) (NTS)
$ zgrep -cE 'CVE-2026-10961[89]' /usr/share/doc/php8.3-cli/changelog.Debian.gz
0
$ php /w/patch-check.php
…
RESULT NOT fixed
### after: apt-get install php8.3-cli (latest)
$ dpkg-query -W php8.3-cli
php8.3-cli 8.3.6-0ubuntu0.24.04.13
$ php -v | head -1
PHP 8.3.6 (cli) (built: Oct 19 2026 15:16:58) (NTS)
$ zgrep -cE 'CVE-2026-10961[89]' /usr/share/doc/php8.3-cli/changelog.Debian.gz
3
$ php /w/patch-check.php
…
RESULT fixed
RESULT is the one-file check. My “before” is the build Ubuntu 24.04 shipped with. The fixed build only differs in its package revision.
The count only says the changelog mentions the CVEs. List the IDs instead:
$ dpkg-query -W php8.3-fpm
php8.3-fpm 8.3.6-0ubuntu0.24.04.13
$ zgrep -oE 'CVE-2026-10961[89]' /usr/share/doc/php8.3-fpm/changelog.Debian.gz | sort -u
CVE-2026-109618
CVE-2026-109619
Both IDs means the package claims both fixes. On 8.2 and later, Debian and the ondrej PPA move to the real upstream numbers, so there php -v is accurate. For the CLI.
Trap 4: on 8.1, an out-of-memory error becomes a silent 502
PHP 8.1 is past end of life, and php.net shipped no 8.1 release. The PHP security team wrote a backport for it anyway, labelled “Minimal mitigation for EOL version”, and its commit lists both CVEs.
Ubuntu 22.04 ships it as 8.1.2-1ubuntu2.28 and calls it fixed. The ondrej PPA ships it as 8.1.34-9.
My verdict: install the backport, set the stopgaps at the end of this section, and move to 8.3 this sprint.
Don’t treat 8.1 as done. The backport’s own authors call it minimal, and Forge’s guide calls the backports “a stopgap rather than a long-term fix”.
CISA’s alert goes further. For anyone still on “PHP 8.1 or earlier unsupported versions”, it says to “take PHP-based systems offline until they can be patched or updated.”
If a server can’t get the backport, follow that.
The fingerprint can’t tell you it’s there. The backport doesn’t register the new directive, so the route reports false on a build Ubuntu calls fixed:
$ dpkg-query -W php8.1-cli
php8.1-cli 8.1.2-1ubuntu2.28
$ zgrep -cE 'CVE-2026-10961[89]' /usr/share/doc/php8.1-cli/changelog.Debian.gz
3
…
hard limit INI missing
RESULT NOT fixed
On 8.1, check two things instead. The changelog names both IDs, the same zgrep -oE as above. And the FPM master runs the installed file: readlink /proc/<pid>/exe doesn’t end in (deleted).
The backport also changes what happens when a request runs out of memory.
I put nginx in front of FPM and requested a script that overshoots an 8M memory_limit by a few megabytes. The script reports fatal errors the way Laravel does, from a shutdown function. Laravel registers one and reads error_get_last() in it.
## Ubuntu 22.04, php8.1 before the fix
…
$ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1/laravel-handler-oom.php
500
$ cat /tmp/laravel.log # what the shutdown handler wrote
local.ERROR: Allowed memory size of 8388608 bytes exhausted (tried to allocate 6291488 bytes)
…
## Ubuntu 22.04, php8.1 after the fix (minimal mitigation)
…
$ curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1/laravel-handler-oom.php
502
$ cat /tmp/laravel.log # what the shutdown handler wrote
(no such file: the handler never ran)
$ tail -n 2 /var/log/php8.1-fpm.log
WARNING: [pool www] child 9669 exited with code 1 after 1.031561 seconds from start
NOTICE: [pool www] child 9680 started
$ tail -n 1 /var/log/nginx/error.log
[error] 9673#9673: *1 recv() failed (104: Unknown error) while reading response header from upstream, …
## Ubuntu 24.04, php8.3 after the fix (full patch)
…
500
…
local.ERROR: Allowed memory size of 8388608 bytes exhausted (tried to allocate 6295552 bytes)
…
## Ubuntu 24.04 + ondrej/php PPA (what Forge installs), php8.1 after the fix
…
502
…
(no such file: the handler never ran)
$ tail -n 2 /var/log/php8.1-fpm.log
WARNING: [pool www] child 10766 exited with code 1 after 1.040020 seconds from start
…
Before the backport, the request got a 500 and a log line. After it, a 502, no log line, and an FPM worker that exited with code 1. The nginx log said recv() failed instead of the PHP fatal message.
The backport’s change to Zend/zend_alloc.c adds an allocator error path that ends in _exit(1). _exit() ends the process without running shutdown functions, which matches the run.
Your error tracker stays quiet for a different reason than in RAG observability, where nothing threw. Here the request did fail, and the process exited before the shutdown handler ran. Same lesson as a worker timeout that is a SIGKILL, not a failed job: no PHP code runs after the process is gone.
The full fix keeps the 500 for this request.
It has its own limit: past the hard limit the worker exits too, with code 137, and the request gets a 502, as Geniar measured. By default the hard limit is twice memory_limit. Above a 1G memory_limit, it’s the limit plus 1G.
On the full fix, a typical web request has to reach double its limit. On the 8.1 backport, a few megabytes over was enough.
Until you move off 8.1, grep the FPM log for exited with code 1 from cron or your log shipper. Each hit is a request that died without a trace in laravel.log.
Then move to PHP 8.3, this sprint.
Laravel 10 supports PHP 8.1 through 8.3, so you can upgrade PHP without upgrading Laravel. On Forge, install 8.3 next to 8.1, switch the site’s PHP version and run your tests. Skip 8.2: its security support ends on December 31, 2026. The official Docker images and Vapor have no 8.1 rebuild, so there an upgrade to 8.3 is the only fix.
Until you’re on 8.3
php.net lists stopgaps for servers that can’t take a fixed release, and says they “do not make an installation safe”. Three of its PHP settings apply to 8.1:
session.upload_progress.enabled=0, for CVE-2026-109619. It costs nothing for a standard Laravel app. Laravel’s sessions don’t go through PHP’s native session handler.memory_limit=-1, for CVE-2026-109618, set so the app can’t override it. It costs the per-request memory guard. One bad request can take all of the server’s RAM, database included on a single-box server.post_max_size=8M, for CVE-2026-109618 (php.net’s number; 32M if you take uploads). It costs any upload bigger than that.
The first one costs nothing, so set it everywhere. Set the other two if your move to 8.3 is more than a few days out. Put them in the FPM pool, then restart FPM:
; /etc/php/8.1/fpm/pool.d/www.conf
php_admin_flag[session.upload_progress.enabled] = off
php_admin_value[memory_limit] = -1
php_admin_value[post_max_size] = 8M
Settings in php_admin_value and php_admin_flag can’t be overridden with ini_set(). It’s the same pool-level override the Livewire chat post warns about, where a pool’s max_execution_time beats set_time_limit().
Don’t use .user.ini for this.
Some early guidance suggested it, including Securing Laravel’s notice. php.net’s advisory, published later, rules it out: “Do not rely on ini_set() or FPM .user.ini.”
Put memory_limit back once you’re on 8.3. On the fixed builds, -1 turns off both the soft and the hard limit (Geniar).
FAQ
Is Forge going to apply the patch automatically?
No. Forge’s guide says its “automatic security updates do not patch PHP”. Update each server from its PHP tab, restart Octane, Horizon and your queue workers, and ask the route on each server.
Are patched PHP runtimes live on Vapor?
The patched runtimes exist, but you have to redeploy. Laravel first planned to redeploy apps automatically, then changed course: “We previously aimed to auto-deploy the patch but now recommend customers redeploy their applications.” For Docker runtimes, rebuild from laravelphp/vapor:php8x with --pull. On PHP 8.0 or 8.1, Laravel says “no patch is available”. Then ask the route, which runs in the function that serves HTTP.
Are we supposed to take down our running applications until then?
For a server that can’t get the fix, yes. CISA’s alert says to “take PHP-based systems offline until they can be patched or updated.” It puts PHP 8.1 and older in that group too. php artisan down is not a substitute. On Apache with mod_php, the attack runs before Laravel boots.
Would blocking all multipart/form-data requests be enough until the update?
No. php.net lists request limits as one partial measure among several, and the 109618 advisory describes paths that don’t depend on multipart. Blocking it also breaks Livewire’s temporary uploads, which POST to your app unless you upload straight to S3.
Before you close the incident
Each setup needs its own step:
- Forge: update every server. The FPM package restarts its pool. Restart the Octane daemon, then run
horizon:terminate,queue:restartandreverb:restart. Check each pool with the route or the socket check, notphp-fpm8.x -v. - Octane:
octane:reloadis enough on RoadRunner only. On Swoole and FrankenPHP, stop and start. On FrankenPHP, take the latest release and check the PHP version inside./frankenphp. - Herd:
herd php:update all, thenherd restart. - Docker: rebuild with
--pulland redeploy. A pinned tag likeFROM php:8.3.35-fpmdoesn’t move, so bump it. If your Dockerfile saysFROM php:8.x-apache, that image runs mod_php with prefork, the setup php.net rates highest risk. - Laravel Cloud: Laravel patched it in place and restarted every workload. Ask the route anyway.
- Vapor: redeploy, and rebuild Docker runtimes with
--pull. - PHP 8.1: install the backport, check the changelog and
/proc, then move to 8.3. - Every server: keep
session.upload_progress.enabled=0inphp.inior the pool. php.net’s October 23 advisory asks for it because of a related bug, CVE-2026-115560, which it rates unlikely and is handling in the next regular release. Until then, the setting is the fix. Laravel never uses the feature, and the one-file check prints it asupload_progress.
The fixed builds, by source:
| Where you get PHP | Fixed build |
|---|---|
| php.net, official Docker images, Herd, Homebrew | 8.2.35, 8.3.36, 8.4.27, 8.5.12 |
| ondrej PPA (Forge) | the same versions, plus 8.1.34-9 (8.1 backport) |
| Debian 12 / 13 | 8.2.35-1~deb12u1 (DLA-4848-1) / 8.4.27-1~deb13u1 (DSA-6574-1) |
| Ubuntu 24.04 | 8.3.6-0ubuntu0.24.04.13 (php -v says 8.3.6), USN-8948-1 |
| Ubuntu 22.04 | 8.1.2-1ubuntu2.28 (8.1 backport), USN-8948-1 |
| FrankenPHP static binary | the latest release, v1.13.5 or later (embeds 8.5.12; check ./frankenphp version) |
| FrankenPHP Docker images | the upstream fixed versions above |
Then ask each server the same question: one signed URL, a few requests, every pid saying "php2shell_fix":true. On 8.1, use Trap 4’s changelog and /proc checks instead.
Delete the route when you’re done.
The update is done when every process serving requests answers true.