Europe/Paris
Posts

PHP 8.1 in production: the fix exists, but you compile it yourself

October 22, 2026 · 3 min de lecture
For PHP2Shell, the PHP team shipped 8.2.35 through 8.5.12. For 7.4, 8.0 and 8.1 there is no release, only three PHP-x.y-CVE-2026-109618-109619 branches in php-src, to build yourself. That's the day version debt stops being a line in an audit report. 1. A distribution backport. Ubuntu fixed php8.1 on 22.04 LTS. If your PHP comes from the distro's packages, an apt upgrade plus an FPM restart is enough. That says nothing about the next CVE: distribution support has its own calendar. 2. Build the security branch. It is doable, here in a multi-stage Dockerfile:
Dockerfile
FROM debian:bookworm-slim AS build

RUN apt-get update && apt-get install -y --no-install-recommends \
      git ca-certificates build-essential autoconf bison re2c pkg-config \
      libxml2-dev libsqlite3-dev libssl-dev zlib1g-dev libonig-dev \
      libcurl4-openssl-dev libicu-dev libpq-dev

RUN git clone --depth 1 --branch PHP-8.1-CVE-2026-109618-109619 \
      https://github.com/php/php-src.git /src

WORKDIR /src
RUN ./buildconf --force \
 && ./configure --prefix=/opt/php \
      --enable-fpm --enable-opcache --enable-mbstring --enable-intl \
      --with-openssl --with-zlib --with-curl --with-pdo-pgsql \
 && make -j"$(nproc)" \
 && make install
It builds. But from now on, you are the one maintaining a PHP: every extension, every configure flag the official image used to set for you, and the next advisory, for which nobody will publish a branch for 8.1. 3. Migrate. The only option that doesn't come back in six months. The calendar on php.net settles the question:
  • 8.1: security support already over (end of 2025)
  • 8.2: until December 31, 2026
  • 8.3: until December 31, 2027
  • 8.4: until December 31, 2028
Aiming at 8.2 today means doing the migration again in two months. 8.3 is the minimum reasonable target, 8.4 is better if your dependencies follow. First, lock dependency resolution on the target version, not on the PHP installed on the dev's laptop:
Json
{
  "require": {
    "php": ">=8.3"
  },
  "config": {
    "platform": {
      "php": "8.3.36"
    }
  }
}
Then Rector for the mechanical changes:
Php
<?php
// rector.php
declare(strict_types=1);

use Rector\Config\RectorConfig;

return RectorConfig::configure()
    ->withPaths([__DIR__ . '/src', __DIR__ . '/tests'])
    ->withPhpSets(php83: true)
    ->withSkip([__DIR__ . '/src/Legacy']);
And PHPStan configured against the target, not against the runtime:
Neon
# phpstan.neon
parameters:
    level: 6
    phpVersion: 80300
    paths:
        - src
        - tests
During the migration, the CI runs on both branches. You keep shipping on 8.1 while making sure every PR also passes on 8.3:
Yaml
jobs:
  tests:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        php: ['8.1', '8.3']
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: ${{ matrix.php }}
          coverage: none
      - run: composer install --no-interaction --prefer-dist
      - run: vendor/bin/phpunit
Careful: with config.platform.php at 8.3.36, composer install on the 8.1 leg may pull dependencies that require 8.3. During the transition, keep the platform on 8.1 and only raise it when you drop the 8.1 leg. What usually breaks between 8.1 and 8.3/8.4, from the changelogs:
  • dynamic properties, deprecated in 8.2 (#[\AllowDynamicProperties] as a temporary escape hatch, a real property as the fix);
  • "${var}" string interpolation, deprecated in 8.2;
  • utf8_encode() / utf8_decode(), deprecated in 8.2;
  • implicitly nullable parameters (function f(Foo $x = null)), deprecated in 8.4. Rector fixes them with ?Foo.
The step before, from 7.4 to 8.1, is covered in this write-up.
  • Compiling your own branches is tenable for a week, not for a year. It's a bridge to get through the incident, not a strategy.
  • Running Rector on the whole codebase in one PR makes review impossible. One set per PR, one directory at a time on the legacy, and the CI on both versions in between.
  • Deprecations don't break anything until the day they become errors. If E_DEPRECATED is filtered out of the production logs, you find out at the next major version. Keep them visible in CI with PHPUnit's failOnDeprecation="true".
On the maintained branches, the fix was out in under 24 hours. On the others, the delay is the length of your migration.
On this page
Book a call