PHP2Shell (CVE-2026-109618 and CVE-2026-109619) is a pair of critical flaws in the PHP runtime, so every Laravel and Symfony app that serves web traffic inherits them. A Composer update will not fix it. Move to PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35, replace any FrankenPHP binary or image, rebuild containers, and fully restart every server process.
Our PHP2Shell explainer covers the two bugs, the leak and the exploitation dispute. This post answers a narrower question for framework teams: in a Laravel or Symfony stack, where does the PHP binary actually come from, and what must you touch so the patched one serves requests? One Laravel app can run on a Forge server's PHP-FPM, on a FrankenPHP binary Octane downloaded into the repo, on a Vapor runtime, and on a CI runner. Each gets PHP from a different place.
What happened, from a framework team's point of view
| When | What | Source |
|---|---|---|
| 20 Oct 2026 | PHP announces a critical release for 21 Oct, 14:00 UTC; both flaws CVSS v4.0 9.2 | php.net |
| 21 Oct 2026 | PHP ships 8.5.12, 8.4.27, 8.3.36 and 8.2.35 and publishes two GitHub advisories | php.net, php-src |
| 21 Oct 2026 | Laravel News publishes the fixes with Laravel's statement on Cloud, Forge and Vapor | Laravel News |
| 21 Oct 2026, when we checked | The official Docker PHP library lists 8.5.12, 8.4.27, 8.3.36 and 8.2.35 as current | docker-library/php |
| 21 Oct 2026, when we checked | FrankenPHP's latest release is still v1.13.1 from 16 October, with no mention of the PHP fixes | FrankenPHP releases |
PHP's advisory says every PHP installation serving web requests is at risk and names PHP-FPM, Apache mod_php, php-cgi, FrankenPHP and LiteSpeed. Laravel News notes that list covers how most Laravel apps run in production. The Hacker News puts it the other way round: updating an application such as Laravel leaves the PHP runtime beneath it untouched.
What Laravel's guidance says
Laravel News quotes Laravel's statement on its own platforms. Here it is with what each line leaves to you.
| Platform | Laravel's statement (via Laravel News) | What you still own |
|---|---|---|
| Laravel Cloud | Apps are patched automatically once the patch is released | Confirming the PHP version afterwards |
| Laravel Forge | Update the server's PHP through the Forge dashboard; if that is not possible, power the server down | The update, restarts, PHP not managed by Forge, any FrankenPHP binary |
| Laravel Vapor | PHP 8 apps are redeployed automatically; no patch for PHP 7, move to PHP 8 now | PHP 7 environments; Docker runtime environments |
The article repeats PHP's interim workarounds (memory_limit=-1 set administratively, a lower post_max_size, session.upload_progress.enabled=0, and fatal_error_backtraces=0 on 8.5, each followed by a restart of every worker) and warns that ini_set() and .user.ini apply too late. The parent post explains why each is partial. It does not address Octane, Sail or Docker, so the rest of this post fills that gap from each tool's own documentation.
Where PHP comes from in your stack
Keep this table open while you patch. "Fixed by" is what you must change for the patched PHP to be loaded.
| Setup | Where the PHP binary comes from | Fixed by | How to check |
|---|---|---|---|
| Forge with PHP-FPM | Versions installed through the Forge dashboard, each with its own FPM process | Runtime tab, PHP, Update; restart FPM | php-fpm8.4 -v |
| Octane with FrankenPHP binary | A FrankenPHP release saved as frankenphp in the project root, PHP compiled in | A FrankenPHP build on patched PHP | ./frankenphp version |
| Octane with RoadRunner | The rr Go binary starts php worker processes | System PHP package, full restart | php -v |
| Octane with Swoole | Swoole extension inside the system PHP | System PHP package, full restart | php -v |
| FrankenPHP Docker images | dunglas/frankenphp, built on official PHP images | Pull, rebuild, redeploy | php -v in the container |
Official php images | php:8.x-fpm, php:8.x-apache, php:8.x-cli | Rebuild with --pull, redeploy | php -v in the container |
| Vapor native runtime | runtime in vapor.yml, such as php-8.4:al2023 | Laravel's automatic redeploy | A deployed request |
| Vapor Docker runtime | FROM laravelphp/vapor:php84 in your Dockerfile | A redeploy on an updated base | A deployed request |
| Laravel Sail | Ubuntu 24.04 with PHP packages from the Sury repository | sail build --no-cache | sail php --version |
| symfony-docker template | dunglas/frankenphp:1-php8.5 build stage | docker compose build --pull --no-cache | php -v in the container |
| Symfony CLI local server | Your local PHP-FPM | Your local PHP package | symfony local:php:list |
Three rows are where teams lose hours.
The Octane FrankenPHP binary is not your system PHP. Laravel's Octane docs say Octane "will automatically download and install the FrankenPHP binary for you." Octane's installer source shows it fetches the latest FrankenPHP GitHub release and saves it as frankenphp in the project root. FrankenPHP's static build docs describe that binary as one file holding the PHP interpreter, Caddy and FrankenPHP. Patching PHP through Forge, apt or dnf does nothing to it. It does not update itself either: the installer only offers a download when no binary is found or the one found is older than its minimum (1.13.0 in the current source).
The official php:*-apache image is mod_php. Docker Hub says the Apache variant runs PHP as mod_php with mpm_prefork by default. The CVE-2026-109618 advisory says full RCE was validated over HTTP on mod_php with default limits and multi-worker prefork. Many Laravel and Symfony Dockerfiles start from that image. If yours does, it goes first.
Sail is for development. Laravel's Sail docs say Sail relies on artisan serve, and Sail's PHP 8.4 Dockerfile installs PHP packages from the Sury repository. It is lower priority than production, with one exception: sail share gives the container a public URL through Expose. Do not share an unpatched Sail container.
Long-running workers versus PHP-FPM
Octane, FrankenPHP worker mode, RoadRunner and Swoole boot the app once and keep it in memory. FrankenPHP's worker docs list what survives between requests: static variables and properties, globals in the worker script, in-memory caches, and settings changed with ini_set(). That matters for PHP2Shell in two ways.
Exposure: what the advisories say
CVE-2026-109619 is about stale state. Its advisory describes global state a persistent worker keeps after the previous request's memory is freed, which a later request on the same worker can touch. Both requests must hit the same worker; more workers and recycling lower the odds without removing the condition.
| Runtime | CVE-2026-109618 (OOM) | CVE-2026-109619 (stale state) |
|---|---|---|
| mod_php | Full RCE validated over HTTP | Confirmed RCE via the PHP 8.5 fatal backtrace path |
| PHP-FPM | Susceptible by source analysis; no complete exploit shown | Conditional; session upload progress RCE validated on a normal dynamic pool |
| FrankenPHP, including worker mode | Likely susceptible by source analysis, not validated | No early trigger in default config; the advisory says that is not immunity |
| RoadRunner, Swoole | Not discussed | Not discussed |
"Not discussed" is not "not affected." RoadRunner's docs show workers launched with a command like php psr-worker.php, and Swoole is an extension inside PHP, so both run the engine code the fixes change. Upgrade them.
Restarts: a reload is not a runtime upgrade
This is the mistake we expect most in Laravel shops. Octane's docs describe php artisan octane:reload as a graceful restart of the application workers so newly deployed code is loaded. FrankenPHP's admin API has a similar worker restart. Both are for application code.
A running process keeps the executable it started with. FrankenPHP workers live inside the frankenphp process and Swoole runs inside the PHP process that started the server, so a worker reload keeps the old engine. RoadRunner's respawned php workers may pick up a new binary, but do not rely on it. The Hacker News lists long-running PHP workers alongside Apache and PHP-FPM as things to restart after updating. The rule that works everywhere: stop the server process and start it again.
# Supervisor program from your Octane config, e.g. [program:octane]
sudo supervisorctl restart octane:*
# Queue workers and Horizon run on the CLI SAPI; restart them too
php artisan queue:restart
php artisan horizon:terminate # Supervisor starts it again
If you are stuck on an unpatched FrankenPHP binary, lowering Octane's --max-requests (default 500) is a rough counterpart to the advisory's pm.max_requests=1 suggestion for FPM. The advisories do not discuss Octane, it costs performance, and it is not a fix. A better short-term move is serving through patched PHP-FPM, since Octane is optional.
Framework reset hooks do not help
Symfony's runtime docs explain that FrankenPhpWorkerRunner reuses one kernel across requests, so services should implement ResetInterface, or you can set FRANKENPHP_RESET_KERNEL=1 for a fresh kernel per request. Octane resets first-party framework state similarly. These hooks reset application objects. PHP2Shell lives in the engine's internal state, below anything a framework resets. Keep the hooks for correctness, not as a mitigation.
Platform by platform: deploy the patched runtime
1. Laravel Forge
- Open the server's Runtime tab, then PHP. For each installed version, open the dropdown and choose Update. Forge's docs warn "a few seconds of downtime is possible."
- Forge only knows about PHP installed through its dashboard, per its docs. PHP installed by hand needs your own package update.
- Restart PHP-FPM and confirm:
php-fpm8.4 -v
sudo systemctl restart php8.4-fpm
ps -o pid,lstart,cmd -C php-fpm8.4 | head -3 # start time should follow the update
- For Octane sites, restart the Supervisor program. For Octane with FrankenPHP, go to step 3 below.
2. Laravel Vapor
- Native runtimes: Laravel says PHP 8 apps are redeployed automatically. Vapor's docs list PHP 8.2 to 8.5 on Amazon Linux 2 and 2023 and recommend AL2023, since AL2 is unmaintained after 30 June 2026.
- PHP 7: no patch. Change
runtimeinvapor.ymlto PHP 8 and deploy. - Docker runtimes start from a Vapor base image such as
laravelphp/vapor:php84, and Vapor builds the image during your deploys. We could not confirm whether the automatic redeploy covers Docker runtimes, so deploy yourself and check the version that answers requests.
environments:
production:
runtime: 'php-8.4:al2023'
3. Octane with the FrankenPHP binary
- Check what you run.
php artisan aboutreports the CLI PHP, not the PHP inside FrankenPHP.
./frankenphp version # FrankenPHP, PHP and Caddy versions
- Get a FrankenPHP build on patched PHP. Wait for a release built on the fixed PHP (none was listed when we checked on 21 October), or build a static binary yourself; FrankenPHP's docs say
PHP_VERSIONselects PHP at build time. - Replace the file. Octane will not replace a binary that meets its minimum, so rename the old one first, or copy in your own build.
mv frankenphp frankenphp.pre-php2shell
php artisan octane:install --server=frankenphp
./frankenphp version
sudo supervisorctl restart octane:*
- If no patched build exists yet, serve through patched PHP-FPM for now.
4. Docker images
- Find every base image:
grep -rn --include='Dockerfile*' '^FROM' . - Rebuild with a fresh pull. FrankenPHP's docs say its images are rebuilt daily when new official PHP images appear, and its February 2026 advisory GHSA-x9p2-77v6-6vhf tells users building on top to force a fresh pull with
docker build --pull.
docker build --pull --no-cache -t registry.example.com/app:php2shell .
docker run --rm --entrypoint php registry.example.com/app:php2shell -v
# symfony-docker template, per its README
docker compose build --pull --no-cache && docker compose up --wait
- Pinned digests and patch tags such as
php:8.4.26-fpmnever move. Edit them. - Redeploy, then check what is running, not what was built:
kubectl rollout restart deployment/app
kubectl exec deploy/app -- php -v
5. Laravel Sail and the Symfony CLI
Sail's docs give the rebuild sequence. Their version uses docker compose down -v, which also deletes volumes such as your local database; drop -v to keep data.
docker compose down
sail build --no-cache && sail up -d
sail php --version
Locally, the Symfony CLI server uses your installed PHP-FPM, picking a version from .php-version. Update local PHP, then run symfony local:php:list. On servers, Symfony under Nginx or Apache with PHP-FPM follows our fleet patching guide.
CI images and Composer platform config
CI is where an old PHP quietly survives, and the cheapest place to catch one.
setup-php. Its README says "Pre-installed PHP versions are not updated to their latest patch release by default." Set update to true:
- uses: shivammathur/setup-php@v2
with:
php-version: '8.4'
env:
update: true
When php-version is unset, setup-php reads .php-version, then platform-overrides.php in composer.lock, then config.platform.php in composer.json.
Composer config.platform does not describe your runtime. Composer's docs say it lets you fake platform packages so dependencies resolve as if that PHP were present, and warn that dependencies "are not checked correctly anymore." Setting it to 8.4.27 patches nothing. composer check-platform-reqs ignores config.platform and checks the real PHP against your packages' requirements, and Composer recommends it in deployment. It knows nothing about CVEs, so PHP 8.4.26 passes.
Add an explicit floor. This guard fails CI or an image build on an unpatched upstream PHP:
php -r '$min = ["8.2"=>"8.2.35","8.3"=>"8.3.36","8.4"=>"8.4.27","8.5"=>"8.5.12"];
$b = PHP_MAJOR_VERSION.".".PHP_MINOR_VERSION;
if (!isset($min[$b]) || version_compare(PHP_VERSION, $min[$b], "<")) { fwrite(STDERR, "Unpatched PHP ".PHP_VERSION."\n"); exit(1); }
echo "PHP ".PHP_VERSION." OK\n";'
Use it only for upstream builds (official images, setup-php, FrankenPHP). Distro packages with backported fixes often keep older version numbers; search their changelog for the CVE instead.
Verify the patch took
- Process start time later than install time. An FPM master or Octane process older than the update runs old code.
- Ask the serving runtime.
./frankenphp version, orexecinto the running container rather than a fresh one from the same tag. - Every node behind the load balancer. One unpatched node is enough.
- Your test suite on the new runtime. The Hacker News warns that reaching the fix's new hard memory limit "stops shutdown handlers from running," so review
memory_limitandpost_max_sizeand watch heavy import jobs.
What would have helped
Teams that knew which runtime each app used spent 21 October rebuilding. Teams that first had to discover an Octane-downloaded FrankenPHP binary in the repo spent it investigating.
Practices any team can use
- Record the serving runtime per app, for example "Octane on FrankenPHP 1.13.1 binary" or "php:8.4-apache, mod_php," in the README or service catalog.
- Treat downloaded runtime binaries as dependencies. Pin
frankenphporrr, record the source, and keep one command that replaces it. - Write the restart rule down:
octane:reloadfor code, a process restart for the runtime. - Rebuild images weekly with
--pull, so a security rebuild is routine. - Fail CI on an old patch level with the guard above and
update: true. - Leave session upload progress off if unused; PHP suggests keeping it disabled after patching.
- Leave end-of-life PHP early. Vapor had no PHP 7 patch, and PHP 8.2 reaches end of life on 31 December 2026, per php.net.
Where Safeguard fits
Safeguard would not have prevented PHP2Shell, and it does not patch servers or restart workers. It helps with where the vulnerable runtime sits and with making the change.
- Images.
safeguard scan --image <name>builds an SBOM of an image and matches components against vulnerability databases, with JSON results under ./output. That works best where PHP came from OS packages. For PHP compiled into an image from source or into a static FrankenPHP binary, confirm withphp -vor./frankenphp version. - Which apps. The platform shows which projects contain a given component and version. Our SBOM incident response post walks through it.
- Gates. Policies and gates evaluated in the platform can block a build still carrying a vulnerable PHP package.
- The change itself. The Safeguard CLI's assistant can edit a Dockerfile to a patched base tag and run your build and tests, asking before each command under its default mode. The desktop and phone apps can send that work to a computer running the CLI for step-by-step approval.
Limits: a composer.lock scan will not find PHP2Shell, because PHP is not a Composer package. Reachability analysis does not lower priority, since the bug fires before your code runs. Safeguard cannot see inside Laravel Cloud or Vapor's managed runtimes, or Forge servers you never scan, and same-day advisories can reach databases late.
Zero-Day Discovery and Gold
- Zero-Day Discovery looks for previously unknown flaws before a CVE exists, in an agentic mode and a deterministic scanner mode, and issues findings as SGZ advisories before they reach public CVE databases. It covers seven package ecosystems.
- Gold Open Source (gold.safeguard.sh, free, no login) offers searchable CVE intelligence with CVSS, EPSS, KEV status, affected and fixed versions and a recommended safe version, a free read API, and a CI gate GitHub Action. It had no entry for these two CVEs when we wrote this, so use php.net and your vendor.
- Gold Registry (registry.safeguard.sh, paid) provides hardened drop-in components and container images held to zero exploitable critical or high CVEs, SLSA L3+ and Sigstore-signed, with evidence pages; an artifact loses its badge on regression. The habit it supports is the one that mattered here: pull patched, signed components from a curated source and rebuild fast.
To try the CLI, download Safeguard at https://safeguard.sh/download and read the CLI docs at https://docs.safeguard.sh/docs/cli .
Common mistakes and troubleshooting
- Running
composer updateand stopping. Framework releases change app code, not the runtime. - Patching Forge PHP while Octane serves through
./frankenphp. The binary carries its own PHP. - Using
octane:reloadafter a runtime upgrade. Restart the server process. - Trusting
php artisan about. It reports CLI PHP, which may not be the serving PHP. - Rebuilding on a cached layer or pinned digest. Use
--pull --no-cacheand edit pins. - Bumping
config.platform.php. It changes dependency resolution, not the runtime. - Setting workarounds with
ini_set()or.user.ini. They apply too late. - Octane fails to start on the new binary. Check it is executable and the right build; Octane's installer picks a glibc build when
getconf GNU_LIBC_VERSIONsucceeds. Fall back to PHP-FPM while you sort it out.
For how the disclosure played out more widely, see governments taking websites offline over PHP2Shell and AI found vulnerabilities: PHP2Shell and PwnEscape.
FAQ
Does a Laravel or Symfony update fix PHP2Shell?
No. The flaws are in the PHP runtime. You need PHP 8.5.12, 8.4.27, 8.3.36 or 8.2.35, or a vendor build with backported fixes, then a restart of every serving process.
Is Octane safer or riskier than PHP-FPM here?
The advisories do not say. CVE-2026-109619 depends on state persisting in a worker, which is how Octane works. FrankenPHP worker mode had no early trigger in default config, which the advisory says is not immunity. RoadRunner and Swoole were not assessed. Patch either way.
Why is my Octane app on the old PHP after a Forge update?
With FrankenPHP, Octane runs the frankenphp binary in your project root, which has PHP compiled in. Forge updates the system PHP. Replace the binary, or use PHP-FPM until a patched build exists, then restart Octane.
Do I need to act on Laravel Cloud or Vapor?
Laravel says Cloud apps are patched and Vapor PHP 8 apps redeployed automatically. Vapor PHP 7 apps get no patch. For Vapor Docker runtimes, redeploy and confirm the version.
Should I change config.platform.php?
No. It only tells Composer which PHP to resolve for. Add an explicit version floor check in CI instead.
My image uses php:8.3-apache. How urgent is this?
Very. That variant runs mod_php with prefork, where both advisories confirmed remote code execution. Rebuild with --pull, redeploy and verify today.
Fix it without waiting for upstream
In incidents like this one, most of the risk sits in two gaps. The first is the time between a fix being published and that fix actually running in your production builds. The second, and worse, is the time before any fix exists at all, while a maintainer works out a patch and a release schedule. Waiting through both gaps is a habit, not a requirement. Safeguard gives teams three ways to close them.
1. Autonomous remediation: pull requests, not tickets
When a fixed version exists, Safeguard's Griffin models open a tested, policy gated pull request that moves the affected dependency to the safe version. You can run it on one repository or in bulk across every project that carries the vulnerable version. Remediation covers eight categories, including dependencies, licenses, security posture and findings. Reachability analysis ranks the vulnerable code paths your application actually calls first, so the first pull requests you merge are the ones that matter most. Autonomous remediation ships switched off, with every category set to Manual, so your team decides how much runs on its own and what still needs a human approval.
2. Gold Open Source: the safe version in seconds, free
gold.safeguard.sh is free and needs no login. Search any CVE or package to see affected and fixed versions, CVSS, EPSS, CISA KEV status and a recommended safe version, or use the "Am I affected?" checker. The same data is available through a free read API, README status badges and RSS threat feeds. The safeguard-sh/gold-scan-action GitHub Action can fail a build on a known exploited vulnerability, on any known vulnerability, or never, so a version you have just removed cannot quietly come back in next week's merge.
3. Gold Registry: start from components that are already fixed
registry.safeguard.sh provides hardened, drop in replacements for upstream packages and container images. Every Gold artifact is held continuously to zero exploitable critical or high CVEs, zero malware and a completed license check, with SLSA L3 or higher provenance, a Sigstore signature and a public evidence page you can hand to an auditor. An artifact loses its Gold badge if it regresses. Teams can point their own package directory at the registry so hardened equivalents resolve without changing how developers install anything.
When upstream has no fix yet, or will not ship one, Safeguard first works to get the fix accepted upstream. If the issue is severe or upstream will not move, Safeguard builds a fork, fixes the vulnerability there and publishes a hardened Gold build, so you are not stuck on a maintainer's release schedule. You can also request a hardened, compatibility tested build of a specific package. This fork and patch flow is in early access today, so ask which packages it covers before you plan around it.
| Your situation | What to use |
|---|---|
| A fixed version exists | Autonomous remediation opens the upgrade pull request; Gold Open Source confirms the safe version |
| You need to know today whether you are affected | Gold Open Source lookup and "Am I affected?" checker, free and without login |
| You want vulnerable versions to stop shipping | The Gold scan GitHub Action in CI, plus Safeguard policies and gates |
| Upstream has no fix yet, or will not ship one | A custom Gold request; fork and patch (early access) |
| You want to start clean on new projects | Pull hardened components and images from Gold Registry |
One honest limit: when the vulnerable code lives in a vendor appliance, a hosted service or an operating system runtime you do not build yourself, the fix still has to come from that vendor or distribution. The steps above apply to the open source dependencies and container images you build and ship, which is where most teams carry the largest and least tracked share of their exposure.
See how Safeguard would have helped
These demos show real sessions on other tasks. Each intro says how the same flow applies to PHP2Shell.
The first shows the Safeguard CLI fixing a lodash CVE in a Node repo: it reads the finding, asks before each command, edits the dependency, runs install and tests with approval, and pushes nothing. For PHP2Shell, you would ask it to move your Dockerfile from php:8.4.26-apache to a patched tag and run your build and tests, approving each command the same way.
Click through it at your own pace, or open it full screen.
The second is the desktop app. A CLI session appears in the conversation list, you ask for work on your computer, pick the folder, and approve or deny each command and file edit, ending with an upgrade done and tests passing. For a Laravel team the folder is the app repo, and the work is the base image bump plus replacing the frankenphp binary, each step waiting for your yes.
Click through it at your own pace, or open it full screen.
The third sets up the local runner on your own machine, approves it with a device code, and scans from the terminal. Here you would point it at each PHP image to see which still carry an old PHP package, without the images leaving your network.
Click through it at your own pace, or open it full screen.
Quick check
After patching PHP on a Forge server, is php artisan octane:reload enough?
No. A reload restarts application workers inside the running server, which keeps the old engine. Restart the Octane process through Supervisor, and if Octane uses FrankenPHP, replace the frankenphp binary too.
Does setting config.platform.php to 8.4.27 patch anything?
No. It only tells Composer which PHP to assume when resolving dependencies. The runtime is unchanged, and composer check-platform-reqs ignores that setting.
Sources
- PHP news archive: advance notice, advisory and 21 Oct releases (php.net)
- GHSA-w8hw-w9fx-j79w, CVE-2026-109618 (php-src)
- GHSA-6wmf-4rv5-p3rp, CVE-2026-109619 (php-src)
- Laravel News: PHP critical RCE security releases
- The Hacker News: PHP issues emergency patches for two critical flaws
- Laravel docs: Octane
- Laravel Octane source: FrankenPHP installer
- Laravel docs: Sail
- Laravel Sail PHP 8.4 runtime Dockerfile
- Laravel Forge docs: PHP
- Laravel Vapor docs: environments
- FrankenPHP docs: Docker
- FrankenPHP docs: worker mode
- FrankenPHP docs: static binaries
- FrankenPHP releases
- FrankenPHP advisory GHSA-x9p2-77v6-6vhf
- Docker Hub: official php image
- docker-library/php versions.json
- symfony-docker template
- symfony-docker Dockerfile
- Symfony docs: Runtime component
- Symfony docs: Symfony CLI local server
- RoadRunner docs: PHP workers
- setup-php README
- Composer docs: config
- Composer docs: command-line interface
- Composer docs: runtime and platform check
Last reviewed: 21 October 2026. This is a developing story; check the linked advisories for the latest status.