PHP2Shell: Why Laravel didn’t protect your app, and how I patched mine

PHP2Shell: Why Laravel didn’t protect your app, and how I patched mine

PHP2Shell hit the PHP engine before Laravel could run a single line. Here’s how I patched my clients’ Dockerized apps, why PECL became the bottleneck, and how I added defense-in-depth mitigations to the Filament Mastery starters.

All my client apps running PHP 8.4 in Docker were on 8.4.27, tested and redeployed, in under four hours. Getting there was mostly a waiting game, and the slowest part had nothing to do with the code.

If you missed the last week: PHP shipped emergency fixes for two unauthenticated remote code execution bugs, nicknamed PHP2Shell. Two days later it disclosed a third, still unpatched. Here is the timeline.

Date (UTC) What happened
Oct 26 CVE-2026-109618 and CVE-2026-109619 added to the CISA Known Exploited Vulnerabilities catalog
Oct 23 PHP discloses CVE-2026-115560, no fix yet, mitigation by configuration
Oct 21, 14:00 PHP releases 8.5.12, 8.4.27, 8.3.36 and 8.2.35
Oct 21, 07:43 Cloudflare reports the first attacks, six hours before the fix
Oct 20 Details leak on social media ahead of the planned disclosure

Both October 21 bugs are rated 9.2 (CVSS v4.0) and affect every PHP version back to 5.4. No login, no specific app code needed.

Why Laravel did not protect you

The attack hits the PHP engine while it parses the incoming request, before your first line of Laravel code runs. Your middleware, your policies and your validation rules never get a chance to see it.

A common reflex is: "Laravel has its own session driver, so session bugs don't concern me, right?" They do. The known exploit path goes through PHP's session upload progress feature, which is handled by the engine during multipart parsing. Laravel's session driver sits one layer above and is irrelevant here.

Two consequences for a Laravel or Filament app:

  • Updating PHP is the only complete fix. PHP says so explicitly in its release notes (php-src 8.4.27).
  • Turning off session.upload_progress.enabled blocks the known paths, not every variant. It also has to be set in php.ini or the FPM pool: a .user.ini or an ini_set() call applies too late in the request. Laravel itself does not use this feature, so you lose nothing by turning it off.

Why php artisan down protected nothing

I saw many Laravel sites go into maintenance mode on October 21. It gave them a nice 503 page and zero protection.

Maintenance mode works through a storage/framework/down file that public/index.php checks. To run that check, the request has to reach PHP-FPM. And PHP parses the multipart body, upload progress included, while starting the request, before executing the first line of index.php. The attack lands before Laravel knows it is down.

What actually protects you is a request that never reaches PHP. Before the patch, the options were, from lightest to heaviest:

  1. session.upload_progress.enabled = 0 in the PHP config. Minutes to apply, site stays up, but it only blocks the known attack paths.
  2. A 503 served by the web server. A return 503; in Nginx before fastcgi_pass, with a static page. Full protection, visitors still get a proper message.
  3. Taking the site offline entirely. Same protection as option 2. Several Dutch sites, the NCSC among them, did exactly that on October 21.

How I patched my clients

Most of my clients run PHP 8.4, everything in Docker on php:8.4-fpm-alpine, with GitLab CI/CD. The target was 8.4.27. Here is what the day looked like.

  1. Wait for the official image. The PHP release was out, but the Docker image took a while to follow. At first the new tags were listed with no usable image behind them, as if they had been published and then rebuilt. I waited until a pull actually returned 8.4.27.
  2. Rebuild from the new base. My build jobs always pull the base image, so with a floating tag like 8.4-fpm-alpine the rebuild picked up 8.4.27 instead of a stale cached layer. Without that pull, a green pipeline can still ship the old PHP.
  3. Run the full CI pipeline, unchanged. Security checks, unit tests, feature tests and GitLab container scanning. A PHP patch release should not break anything, but an emergency release is exactly when you want proof rather than faith.
  4. Deploy through the normal CD pipeline. No manual hotfix on the servers. Recreating the containers also restarts queue workers and schedulers, which keep the old PHP in memory otherwise.

Total: under four hours for all of them.

The real bottleneck was PECL. During the rebuilds, pecl.php.net stopped responding. My guess, which I can't prove: half the PHP world was rebuilding images at the same moment, and every Dockerfile with a pecl install xdebug or pecl install imagick was hitting the same server. My pipelines could not finish until it came back.

The October 23 CVE and the starters update

The Filament Mastery starters now ship with session upload progress turned off. A standard Laravel app is very unlikely to be exposed to the October 23 bug, and this change is defense in depth.

CVE-2026-115560 has no fix yet. PHP will publish it in its next regular release. Updating to 8.4.27 does not cover it. An app is exposed only if all three conditions hold:

  1. session.serialize_handler is set to php_binary or php_serialize
  2. the code switches it to php with ini_set() before calling session_start()
  3. session upload progress is enabled, which is PHP's default

Laravel's standard session handling does not normally use PHP's native session_start() flow. However, packages or custom code may do so, so check your actual dependency stack before concluding that the application is not exposed. The fix PHP recommends in the meantime is the same setting already advised for CVE-2026-109619 on October 21. One line covers both, so the Filament Mastery starters carry it by default.

; docker/common/php-fpm/conf.d/php.ini
; PHP2Shell (CVE-2026-109619) and CVE-2026-115560 mitigation
session.upload_progress.enabled=Off

Copied into the image:

RUN if [ "$APP_ENV" = "local" ] ; then \
        cp "$PHP_INI_DIR/php.ini-development" "$PHP_INI_DIR/php.ini" ; \
    else \
        cp "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" ; \
    fi

COPY docker/common/php-fpm/conf.d/php.ini "$PHP_INI_DIR/conf.d/custom.ini"

The full setup, with this config already applied, is in my production-ready Docker setup for Laravel Filament, updated today.

The official image reads that directory for both CLI and FPM. If you are on PHP 8.5, also add fatal_error_backtraces = Off: PHP lists it as the second mitigation for CVE-2026-109619 on that version. Keep both settings even after patching, until PHP ships the fix for CVE-2026-115560.

The Filament Mastery starters are updated and available now, with this config included by default:

Laravel Filament Backend Starter - Build your admin panel fast
Kickstart your Laravel v13 projects with Filament PHP v5. Secure, customizable backend panel ready for unlimited projects.
Laravel Filament Multipanel Starter - Build your app fast
Launch Laravel apps faster with this Filament PHP starter. Includes ready-to-use backend & member panels, user roles, Horizon, and more.
Laravel Filament Multi-Tenant Starter - Build your app fast
Building a SaaS with Laravel Filament? This starter gives you team-based multi-tenancy out of the box. Backend panel, tenant member panel, team invitations, team switching, and tenant-aware permissions.

Checklist for your Laravel and Filament apps

  • [ ] PHP is at 8.5.12, 8.4.27, 8.3.36 or 8.2.35: docker compose exec php-fpm php -v
  • [ ] On PHP 8.1 or older, php -v shows the same number before and after a distro backport: check the package revision instead, then plan the upgrade (Laravel Forge)
  • [ ] Images rebuilt with a fresh pull of the base image, not from cache
  • [ ] Queue workers, Horizon, Octane and Reverb restarted (recreating containers does it)
  • [ ] Upload progress off, set in php.ini or conf.d, not .user.ini: docker compose exec php-fpm php -i | grep session.upload_progress.enabled returns Off
  • [ ] On PHP 8.5, fatal_error_backtraces is Off
  • [ ] Every PHP version on the server patched, including ones no site uses today