Backend 7 min read 1294 words

PHP2Shell: what to do with your PHP servers until the patch arrives

ES
PHP2Shell: what to do with your PHP servers until the patch arrives

Update: the patches are now available (PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35). The fixed versions, the details of both bugs and the official mitigations are in PHP2Shell: the patch is out.

Note: this post was written on the morning of 21 October, before the patch was released. Technical details and fixed versions will be published with the update, so check php.net for the definitive information.

On Monday, the PHP security team published an unusual notice: a critical security release scheduled for today, 21 October at 14:00 UTC, fixing two vulnerabilities, CVE-2026-109618 and CVE-2026-109619, both rated 9.2 on CVSS v4.0. They reserve the right to release earlier if the details become public or attacks appear.

PHP doesn’t usually give advance warning, and it did so here because the information had already leaked. Today I had to set up a whole server with Docker and several PHP services, disabling upload progress, so I’m taking the chance to sort out what’s known and what can be done in the meantime.

What’s known

According to Ars Technica, the notice came after details of the flaw leaked, including a video of a tool called PHP2Shell running code on servers with WordPress, Magento, Drupal, Joomla, Laravel, Symfony, MediaWiki or phpBB. Security firm watchTowr has confirmed the vulnerabilities are real, and Sansec expects mass attacks within 15 minutes of the patch being released, which is how long it’ll take someone to compare the code before and after.

What isn’t confirmed yet:

  • Affected versions. The leak talks about everything since PHP 5 (Sansec says since 5.4), but PHP won’t say until the patch is out. What is known is that only supported versions, 8.2 onwards, will get the official fix.
  • The exact bug. A public analysis points to a use-after-free in PHP’s session upload-progress code, but the PHP team hasn’t confirmed it.

The key point is that the flaw is in PHP itself, before the application code runs. It can’t be fixed from WordPress, Laravel or whatever you use: PHP has to be updated.

Mitigations, and their limits

Until the patch is out, some measures reduce the risk. Mattias Geniar has published a very thorough mitigation guide, with a clear warning: none of them is proven against the real exploit.

The most widespread one is disabling session upload progress, session.upload_progress, the feature that lets you show a progress bar while a file uploads. If the leak is right, the exploit uses that path. Almost no application uses it, so disabling it doesn’t break anything.

The problem is that this only covers one of the two vulnerabilities. There’s talk of a second path through the processing of multipart requests, and for that one there’s no known mitigation inside PHP. The only measure that might cover it is blocking multipart requests before they reach PHP, at the cost of breaking every file upload.

MitigationUpload progressSecond multipart pathSide effect
session.upload_progress.enabled = 0CoveredProbably notNative progress bars stop working
Block multipart at the web serverCoveredMaybeFile uploads not explicitly allowed break
Update PHPFixedFixedNone

And some things don’t help, even though they look like they would: file_uploads = Off (PHP still processes the whole request body), lowering post_max_size, enable_post_data_reading = Off, checks inside the application, or a WAF if the origin server can be reached directly.

Disabling upload progress

On a traditional server a file in each SAPI’s conf.d directory is enough, for example /etc/php/8.4/fpm/conf.d/99-disable-upload-progress.ini:

session.upload_progress.enabled = 0
session.use_strict_mode = 1

Sansec recommends session.use_strict_mode on top of the first one. It’s good practice in general, although it has no effect if sessions are stored in Redis or the database.

With PHP-FPM, the safest option is to set it in every pool as well, with php_admin_flag so it can’t be changed from code or from an .htaccess:

; /etc/php/8.4/fpm/pool.d/www.conf
php_admin_flag[session.upload_progress.enabled] = off
php_admin_flag[session.use_strict_mode] = on

Then reload the workers with systemctl reload php8.4-fpm, repeating it for every installed PHP version.

With Docker

In the official PHP images, additional configuration is read from $PHP_INI_DIR/conf.d, which is /usr/local/etc/php/conf.d. The easiest approach, especially with several services, is to keep a single .ini file and mount it in every container:

services:
  app:
    image: php:8.4-fpm
    volumes:
      - ./php/99-php2shell.ini:/usr/local/etc/php/conf.d/99-php2shell.ini:ro

  worker:
    image: php:8.4-cli
    volumes:
      - ./php/99-php2shell.ini:/usr/local/etc/php/conf.d/99-php2shell.ini:ro

Or, if you build your images, add it in the Dockerfile:

FROM php:8.4-fpm
RUN printf "session.upload_progress.enabled = 0\nsession.use_strict_mode = 1\n" \
    > "$PHP_INI_DIR/conf.d/99-php2shell.ini"

In both cases the containers have to be recreated to pick up the new configuration. And it’s worth checking every image: it’s easy to remember the web service and forget the admin panel, the cron job or that old image someone built years ago on a different base.

Checking it’s active

The value that matters is the one PHP-FPM sees when serving a request, not the command-line one, which may read a different php.ini:

# The effective value for FPM (should say Off)
php-fpm8.4 -i | grep session.upload_progress.enabled

# In a container with the official image
docker compose exec app php-fpm -i | grep session.upload_progress.enabled

The most reliable way is to ask the application itself with var_dump(ini_get('session.upload_progress.enabled'));, which should return "0".

Blocking multipart at the web server

For the second path, the proposed measure is cutting off multipart requests before PHP. In nginx:

# In the http {} context
map $http_content_type $block_multipart {
    default       0;
    "~*multipart" 1;
}

# Inside every server {} that passes requests to PHP
if ($block_multipart) {
    return 403;
}

This breaks every file upload. Mattias’s guide includes a variant that allows multipart only on specific upload routes, although those routes stay exposed until you patch, plus examples for Apache, Caddy and lighttpd. To check it from outside, this request should return a 403:

curl -s -o /dev/null -w '%{http_code}\n' -F a=b https://example.com/

If the server is behind Cloudflare, they have deployed WAF rules on every plan, but those only help if the origin server accepts traffic exclusively from Cloudflare’s IPs.

If something has to be switched off

For non-essential services, the safest option until the patch arrives is to switch them off. But watch how: Mattias found that if you remove only the PHP handler in Apache, .php files are served as plain text, database passwords included. The right way is to stop the web server or return a 503 for everything.

When the patch is out

  • Update PHP as soon as it’s available. On Debian and Ubuntu, versions patched by the distribution may keep the same PHP version number, so look at the package version.
  • Restart everything that holds PHP in memory: PHP-FPM, Apache with mod_php, Laravel Octane, FrankenPHP and queue workers. A long-running process keeps running the old code even if the package is updated.
  • In Docker, rebuild the images with the new version and recreate the containers. If you use tags like php:8.4-fpm, make sure you pull the new image rather than using the cached one.
  • PHP 8.1 or older won’t get an official patch: either migrate, or keep the mitigations and rely on your provider or distribution publishing a fix.

Disabling upload progress can stay that way after patching: almost nobody uses it, and it’s one less attack surface.

My takeaway

This week has already shown how eight certificates with one-byte keys forced certificate stores to be updated across half the world, and now it’s PHP’s turn. In both cases, the hard part isn’t applying the patch, but knowing where to apply it: every container, every PHP version, every forgotten service.

Today is about taking inventory, applying the mitigations knowing they’re partial, and having everything ready to update at 14:00 UTC. Mitigations buy time, but the only solution is the patch.

Sources: PHP security team announcement, Critical PHP RCE: CVE-2026-109618 and CVE-2026-109619 by Sansec, PHP CVE-2026-109618 and CVE-2026-109619: early mitigation steps by Mattias Geniar, and Ars Technica.