Dev.to Security 🔐 Cybersecurity 👁 0 📖 7 min read

Stopgap fix: patch CVE-2026-109618 / 109619 (PHP RCE) while still on PHP 7.4

On 2026-10-21 the PHP project disclosed two unauthenticated remote code execution vulnerabilities (php.net announcement): CVE-2026-109618 (GHSA-w8hw-w9fx-j79w): out-of-memory handling can interrupt PHP while internal

On 2026-10-21 the PHP project disclosed two unauthenticated remote code execution vulnerabilities (php.net announcement):

  • CVE-2026-109618 (GHSA-w8hw-w9fx-j79w): out-of-memory handling can interrupt PHP while internal request data is only partly initialized, and request cleanup may then treat that invalid data as valid.
  • CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp): internal state owned by a request can survive after its memory is freed, and a later request on the same worker may use it.

Every PHP installation that serves web requests is affected: PHP-FPM, Apache mod_php, php-cgi and others. The advisory rates the risk highest for mod_php, where an unauthenticated request can trigger the attack before any application code runs.

Fixed releases exist only for PHP 8.2.35, 8.3.36, 8.4.27 and 8.5.12. PHP 7.4 is end of life, but because the bugs are so severe, the PHP security team published minimal source backports for 7.4, 8.0 and 8.1.

The official Docker images php:7.4-* will not be rebuilt. If your Dockerfile says FROM php:7.4-apache, you stay vulnerable unless you act.

So I rebuilt those images with the backport applied and published them as open source:

👉 https://github.com/softjapan/php74-cve-patched

⚠️ This is a stopgap, not a fix for PHP 7.4.

  • Only CVE-2026-109618 and CVE-2026-109619 are fixed. Other issues found after 7.4.33 are not.
  • The base OS, Debian 11 (bullseye), reached end of LTS in 2026-08. OS packages such as OpenSSL and Apache receive no more updates.
  • These are not official images from the PHP project or Docker.

The real fix is upgrading to PHP 8.2 or newer. Use these images to buy time for that upgrade.

TL;DR

- FROM php:7.4-apache
+ FROM ghcr.io/softjapan/php74-cve-patched:7.4-apache
  1. Change one FROM line (apache / fpm / cli, amd64 / arm64).
  2. Rebuild, redeploy and restart every PHP worker.
  3. Confirm the fix with check-patched.sh.
  4. Plan your upgrade to PHP 8.2+. You are done only after this step.

Step 1: check whether you are vulnerable

The catch: after the backport, php -v still prints 7.4.33. The version number cannot tell you whether you are protected.

This script looks inside the PHP binaries instead:

curl -fsSLO https://raw.githubusercontent.com/softjapan/php74-cve-patched/main/scripts/check-patched.sh
sh check-patched.sh php:7.4-apache            # an image
sh check-patched.sh --container my-web-1      # a running container
sh check-patched.sh --local                   # this machine (VM / bare metal)

The official image:

Image: php:7.4-apache
PHP version: 7.4.33
  VULNERABLE  /usr/lib/apache2/modules/libphp7.so
  VULNERABLE  /usr/local/bin/php
RESULT: VULNERABLE to CVE-2026-109618 / CVE-2026-109619

The patched image:

Image: ghcr.io/softjapan/php74-cve-patched:7.4-apache
PHP version: 7.4.33
  patched     /usr/lib/apache2/modules/libphp7.so
  patched     /usr/local/bin/php
RESULT: PATCHED (backport found in every PHP binary)
  • PHP 8.2.35 / 8.3.36 / 8.4.27 / 8.5.12 and newer count as fixed. Older versions are checked for zend_new_array_singleton, a function the backport adds.
  • It checks each SAPI binary separately: the CLI, plus libphp7.so (mod_php) and php-fpm, which are what actually serve your requests. That catches a patched CLI next to an unpatched Apache module.
  • Exit code 0 means patched and 1 means vulnerable, so you can block vulnerable images in CI.

The check is a heuristic. A backport from a distribution or LTS vendor written a different way may show as vulnerable. In that case, check with your vendor.

Step 2: switch images

The same images, with the same tags and digests, are on two registries:

Official image GHCR Docker Hub
php:7.4-apache ghcr.io/softjapan/php74-cve-patched:7.4-apache softjpn/php74-cve-patched:7.4-apache
php:7.4-fpm ghcr.io/softjapan/php74-cve-patched:7.4-fpm softjpn/php74-cve-patched:7.4-fpm
php:7.4-cli ghcr.io/softjapan/php74-cve-patched:7.4-cli softjpn/php74-cve-patched:7.4-cli

They work exactly like the official images:

  • docker-php-ext-install, docker-php-ext-configure, pecl and php.ini-production all work as before.
  • CI checks that php -m lists the same extensions as the official image.
  • apt-get install works, because apt now points at archive.debian.org.

Example for Laravel:

FROM ghcr.io/softjapan/php74-cve-patched:7.4-apache

RUN apt-get update && apt-get install -y --no-install-recommends \
        libzip-dev libpng-dev libjpeg-dev libfreetype6-dev \
    && rm -rf /var/lib/apt/lists/* \
    && docker-php-ext-configure gd --with-freetype --with-jpeg \
    && docker-php-ext-install -j"$(nproc)" gd pdo_mysql zip bcmath opcache \
    && a2enmod rewrite

Changing the image is not enough on its own. Containers and workers that are already running keep the old binaries. Rebuild, redeploy and restart every PHP worker, then run check-patched.sh --container <name> against the running container.

What exactly changed

Very little. The images start from the last PHP 7.4 Dockerfiles in docker-library/php (commit 1361ba95) and change three things:

  1. apt uses archive.debian.org. Bullseye has left the regular mirrors, so apt-get update there returns 404.
  2. The backport is applied to the PHP 7.4.33 source with patch -p1 --fuzz=0. If it does not apply cleanly, the build fails.
  3. The build checks the result. If php, php-fpm or libphp7.so is missing the fix, the build fails.

The patch is byte-identical to the upstream commit php/php-src@712e60f. On every push and every week, CI checks two things:

  • The vendored patch still matches the upstream commit.
  • The upstream backport branch has no new commits. If it does, CI fails so a new patch can be shipped.

My first internal version re-ran ./configure on top of php:7.4-apache. That approach hard-coded x86_64 options and let the fpm and cli builds drift from each other. Starting from the official Dockerfiles keeps the images compatible with the official ones and makes multi-arch builds straightforward.

Trust, but verify

You probably should not pull a security image from a stranger on faith, so everything here can be checked:

  • Images are built only by GitHub Actions in the repository, with SBOM and provenance attestations.
  • Images are signed with cosign (keyless):
cosign verify ghcr.io/softjapan/php74-cve-patched:7.4-apache \
  --certificate-identity-regexp '^https://github.com/softjapan/php74-cve-patched/' \
  --certificate-oidc-issuer https://token.actions.githubusercontent.com
  • Pin a digest (@sha256:...) in production.
  • Or skip these images and build them yourself. The guide also covers applying the patch with patch -p1 if you build PHP from source without Docker.

Mitigations if you cannot switch right away

The advisory also lists mitigations. It is clear that they only block some attack paths and do not make an installation safe, but they are useful as a bridge until you switch, and as defense in depth afterwards.

CVE-2026-109618 (OOM corruption)

  • Set memory_limit=-1 in a way applications cannot override, such as php_admin_value in the FPM pool or Apache config. Check that no other setting brings back a finite limit.
    • Use a container or cgroup memory limit instead, ideally one that kills only the affected worker.
    • Do not use RLIMIT_AS or RLIMIT_DATA as a substitute. They can cause the very allocation failure you are trying to avoid.
  • Lower post_max_size: 8M if you have no file uploads, 32M if you do. Keep upload_max_filesize below post_max_size.
  • Also limit request size, field count, multipart part count, header count and nesting depth at the web server or proxy, including for requests without a usable Content-Length.

CVE-2026-109619 (stale global state)

  • Set session.upload_progress.enabled=0 and restart every worker.
    • Set it in php.ini, the FPM pool or the server config, which apply before request parsing. ini_set() and .user.ini do not help.
  • Remove invalid or deprecated settings applied per request that emit diagnostics before initialization. Hiding the diagnostics with @ or display_errors does not help.
  • Fix fatal errors and timeouts that an attacker can trigger.

The images ship an optional file with some of these settings (session.upload_progress.enabled = 0 and lower post_max_size / upload_max_filesize). It is disabled by default, so PHP behaves exactly like the official image. To enable it:

RUN cp /usr/local/share/php74-cve-patched/zz-cve-2026-mitigations.ini "$PHP_INI_DIR/conf.d/"

After the stopgap

One more time: this stops the bleeding. It does not cure the patient.

  • [ ] Find every PHP 7.4 environment and run check-patched.sh on it.
  • [ ] Switch images, redeploy and restart the workers.
  • [ ] Confirm check-patched.sh --container reports PATCHED on the running containers.
  • [ ] Add the mitigation settings where they make sense.
  • [ ] Set a plan and a deadline for moving to PHP 8.2+. PHP 8.2 itself reaches end of life on 2026-12-31, so consider 8.3 or newer.
  • [ ] Tell your team that the OS (Debian 11) is not getting updates either.

Summary

  • CVE-2026-109618 and CVE-2026-109619 are unauthenticated RCEs. Treat mod_php as top priority.
  • The official PHP 7.4 Docker images will not be fixed. If you use php:7.4-*, you stay vulnerable.
  • php74-cve-patched is a stopgap that brings in the official backport with a one-line FROM change.
  • The real fix is upgrading to PHP 8.2 or newer.

PHP 8.0 / 8.1 images (official backports exist for those too) and Alpine variants are on the roadmap. Open an issue if you need one. And if this saved you an emergency upgrade, a ⭐ on GitHub helps other people stuck on PHP 7.4 find it.

👉 https://github.com/softjapan/php74-cve-patched

References

📰 Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.