PHP 8.3.36 still starts with upload progress enabled

John Burns

PHP 8.3.36 still starts with upload progress enabled

php -v on the official php:8.3.36-cli-bookworm image printed PHP 8.3.36 (cli) (built: Oct 21 2026 15:59:51) (NTS). The same process reported session.upload_progress.enabled as 1. ini_set() could not change that value. A .user.ini in the working directory that set it to 0 changed the FPM response and left the CLI reading 1. The 21 October security release fixed two remote-code-execution bugs. It did not change this default. A version check also does not show whether the worker that is serving traffic loaded the setting before it accepted the next request.

That gap is the part an inventory misses. PHP’s 23 October note still asks operators to keep upload progress off until a later bugfix, for a narrow configuration that the patched CLI still accepts. The 26 October Windows packages then added a -1 suffix that PHP_VERSION does not contain. The check that matches those announcements is the value in the running worker, the file that set it, and a request made after that worker was restarted.

What the October releases fixed, and what they left on

On 21 October 2026 the PHP project shipped 8.2.35, 8.3.36, 8.4.27, and 8.5.12 for CVE-2026-109618 and CVE-2026-109619. The first is an out-of-memory bailout that can leave request state only partly initialized. The second is request-owned state that can survive into a later request on the same worker, before normal initialization replaces it. PHP’s advisory says updating is the only complete fix. It also names two settings that remove confirmed paths when an immediate update is not possible: session.upload_progress.enabled=0 for the session path, and on 8.5 fatal_error_backtraces=0 for the confirmed backtrace path. Neither setting replaces the update. Hiding warnings, changing error_reporting, or suppressing display of errors does not close the early path. The advisory says the global state is touched before PHP decides whether to report the diagnostic.

The 23 October note is narrower, and it is not a claim that the 21 October patch failed to land. It describes an unlikely remaining path, which PHP said it will publish as CVE-2026-115560 with the next bugfix release. The path needs three conditions together. The configured serialize handler is php_binary or php_serialize. Application code switches it to php with ini_set() before session_start(). Upload progress is still enabled. PHP asked operators who already turned upload progress off to leave it off. Applications that never change the serialize handler at runtime can keep the feature. The note does not say the feature is off by default. It is not.

The 26 October Windows packages are 8.5.12-1, 8.4.27-1, 8.3.36-1, and 8.2.35-1. The announcement says the -1 suffix marks a dependency rebuild with Brotli 1.2.1, for CVE-2026-111497, and that the PHP source version is unchanged. In those Windows builds the decoder can be reached through the cURL extension when automatic response decompression is enabled. That is a package fact. It is not a field in PHP_VERSION.

This comparison did not attempt either remote-code-execution path, and it did not send a crafted upload. It recorded defaults, access modes, and which configuration surface a worker actually used.

Eight official images, one default

The CLI images were the official bookworm tags php:8.2.34-cli-bookworm, 8.2.35, 8.3.35, 8.3.36, 8.4.26, 8.4.27, 8.5.11, and 8.5.12. Each container ran with network access disabled, and none published a host port. Image creation times split on the release day. The four unpatched tags were created between 00:26 and 00:37 UTC on 21 October. The four patched tags were created between 15:50 and 16:04 UTC the same day. A tag can move. The digests below are the builds that produced these results.

PHP_VERSION matched the tag on every image. session.upload_progress.enabled was 1 on every image, patched and unpatched. The frequency was 1%, the minimum frequency was 1, the field name was PHP_SESSION_UPLOAD_PROGRESS, and the prefix was upload_progress_. session.serialize_handler was php. memory_limit was 128M. fatal_error_backtraces does not exist on 8.2, 8.3, or 8.4. On both 8.5.11 and 8.5.12 it was 1.

The access mode for upload progress was 2, which is per-directory. ini_set("session.upload_progress.enabled", "0") returned false and left the value at 1 on all eight CLIs. The same call cannot turn the setting on. A script that prints the value after ini_set is not evidence of the startup value, and it is not a way to apply the mitigation. The call is rejected because of the access mode, not because the binary is patched.

session.serialize_handler has access mode 7. Before any output, ini_set from php_serialize to php returned the previous value, and the following session_start() succeeded. The same switch from php_binary to php succeeded. That pair of results was the same on 8.3.35, 8.3.36, 8.5.11, and 8.5.12. Upload progress remained 1 for those calls. The patched CLI still accepts the handler switch named in the 23 October note. The version bump did not remove it.

Order matters for that probe. If the script prints anything before ini_set, the call fails with a different message: session ini settings cannot be changed after headers have already been sent. A failed ini_set in that order does not mean the handler is locked. It means the probe wrote output first. session_start() then fails too, and the status stays at 1 rather than the active value 2.

fatal_error_backtraces on 8.5 has access mode 7. ini_set to 0 worked on both 8.5.11 and 8.5.12, including after earlier output. The patched 8.5.12 image still starts with the setting on. If the goal is to remove that producer before a request runs, the 0 has to be in the startup configuration. A line that runs inside the request does not cover the early path the advisory describes.

A directory file and a pool flag are not the same lock

Upload progress can be set at process start. On the 8.3.36 CLI, php -d session.upload_progress.enabled=0 reported 0. A file containing the same assignment, loaded through PHP_INI_SCAN_DIR=/scan, also reported 0. That variable replaces the compiled-in scan directory when it does not end with a colon. php_ini_scanned_files() listed only /scan/99-progress.ini. On this image that dropped docker-php-ext-opcache.ini and docker-php-ext-sodium.ini.

The trailing-colon form is the one that adds a directory instead of replacing the default. PHP_INI_SCAN_DIR=/scan: reported upload progress 0 and still listed the two docker extension files after the lab file. The FPM image has a third scanned file, docker-fpm.ini, which the CLI image does not. Copying a CLI scan list onto an FPM pool, or the reverse, will not describe the other SAPI. Read php_ini_scanned_files() from the SAPI that serves the site.

The CLI does not read .user.ini. A working directory whose .user.ini contained session.upload_progress.enabled = 0 still reported 1 from php -r. The same directory, served by php:8.3.36-fpm-bookworm over FastCGI, reported 0. The response began with X-Powered-By: PHP/8.3.36. A CLI check in the application directory and a request to the worker are not the same measurement. The CLI result sends an operator looking for a setting the worker has already loaded. The request result looks like the mitigation, which is the reading PHP’s advisory says not to trust for the early session path. .user.ini is applied after normal initialization. It can change what the script prints. It is not the administrative setting that applies before request parsing.

The pool syntax decides whether that directory file can undo the change. With php_admin_flag[session.upload_progress.enabled] = off in the FPM pool, a .user.ini that set the value back to 1 lost. The script saw 0, and ini_set to 1 returned false. With php_flag instead of php_admin_flag, the same .user.ini won, and the script saw 1. A pool line that uses php_flag is a default. It is not a lock. A file written under the document root can turn the feature back on for later requests in that directory. php_admin_flag is the pool form the script could not override.

The unpatched php:8.3.35-fpm-bookworm image applied .user.ini the same way. The script saw 0. The 21 October patch did not change per-directory loading. It changed request-state handling inside the binary. A .user.ini that reports 0 is not evidence that the worker is 8.3.36. A worker that reports 8.3.36 is not evidence that upload progress is off.

expose_php is a separate switch, and it is on in the official FPM image. A scanned file containing expose_php=0 removed X-Powered-By from the FastCGI response. The header is a version disclosure. It matched the image tag in the default configuration, and it will not show a Windows package suffix. It is not an inventory of upload progress. Leave it off unless something you control needs it.

The file on disk is not the worker

An FPM container was started with PHP_INI_SCAN_DIR pointed at a directory whose ini file set upload progress to 1. The first request reported 1 and listed that file as scanned. The file was then rewritten to 0 without restarting the worker. The next request still reported 1, and it still listed the same path. After a restart of that container, the next request reported 0.

A configuration check that reads the ini file will say the mitigation is present while the old worker is still running. PHP’s advisory says to restart every worker after the administrative change. This run is the small form of that instruction: one static child, one file, one restart. A reload that leaves old children in the pool keeps the old value on those children until they exit. php_ini_scanned_files() names the path the worker opened at start. It does not prove the worker reread the file after the rewrite.

The same limit applies to a package upgrade. Replacing the binary on disk does not replace a worker that is already running. On FPM, the master has to recycle the children. On a container image, the new image has to be the one the running container was created from. php -v on the host, or in a one-off container of the new tag, does not describe the container that is still serving.

The Windows suffix is not PHP_VERSION

The CycloneDX file for php-8.3.36-1-nts-Win32-vs16-x64.zip names the component php version 8.3.36. The artifact property is php-8.3.36-1-nts-Win32-vs16-x64.zip. The zip SHA-256 recorded in that file is 1a097d52b7a68622cdc723073b61cc70caf79c941218ff65b2d8441dbc6b10d4, which is the digest on the Windows download page for the build dated 26 October 2026 13:05 UTC. The same document lists Brotli 1.2.1, including the package URL pkg:generic/brotli@1.2.1. A probe that only reads PHP_VERSION, phpversion(), or X-Powered-By cannot tell 8.3.36 from 8.3.36-1.

The official Linux image is a different build, not a delayed copy of that Windows package. php:8.3.36-cli-bookworm printed the 21 October 15:59:51 build. Inside the image, libbrotli1 was 1.0.9-2+b6 and libcurl4 was 7.88.1-10+deb12u15. curl_version() reported Brotli support. The 8.3.35 image had the same two Debian package versions. The 26 October announcement scopes the Brotli rebuild to the Windows packages. A Linux container that prints 8.3.36 has the PHP security release from 21 October. It does not have the Windows -1 dependency set, and the distro Brotli package in that image is older than 1.2.1.

This run did not decompress a Brotli response, and it does not say the Debian library is reachable in the same way as the Windows cURL extension. The measured point is smaller. The string 8.3.36 does not identify the Brotli build. On Windows, read the zip name or the CycloneDX artifact property. In this image, read the package list. A host that installs PHP from a distribution may backport either fix under yet another version string. That package has to be checked against the distribution advisory, not against PHP_VERSION alone, and it was not the build under test here.

The digests for the 8.3 tags used in this comparison:

8.3.35-cli  sha256:217b08a558b81d298e102dd2a06ee4b4ed84b03b863334eea824293967aee08a
8.3.36-cli  sha256:cdb77e306d6bb99db40e513fbe036ecc6c53b575cbcb7e581387845fef26d755
8.3.35-fpm  sha256:ca090064188589bba7d1a2106356b15975bcab3d9e40c5b7df45c6e732ee0962
8.3.36-fpm  sha256:6b4641a875dd3e0f7551b2751e1f5909084856d18166af7fbb522ac230feb702

Verification on the running worker

Read the version from the worker that serves traffic. A CLI binary that happens to be first on PATH is a different process. On FPM, use a short script in a directory that has no .user.ini, and make sure the pool is not applying a per-directory override at the same time. Record PHP_VERSION, session.upload_progress.enabled, and on 8.5 fatal_error_backtraces.

<?php
header('Content-Type: text/plain');
echo PHP_SAPI, "\n";
echo PHP_VERSION, "\n";
echo ini_get('session.upload_progress.enabled'), "\n";
echo ini_get('fatal_error_backtraces'), "\n";
echo php_ini_scanned_files(), "\n";

The CLI form of the same question is useful only as a statement about that CLI:

php -v
php -r 'echo PHP_VERSION, " ", ini_get("session.upload_progress.enabled"), " ", ini_get("fatal_error_backtraces"), "\n";'

If that CLI prints 1, it has not proved the FPM worker is on. If it prints 0 because of -d or a scan directory, it has not proved the worker is off.

Set the value where the worker will load it before request parsing, then restart and request the script again. In php.ini:

session.upload_progress.enabled = 0

On 8.5, the same file is the place for fatal_error_backtraces = 0 if that producer has to be off before the request starts. In an FPM pool, prefer the admin form:

php_admin_flag[session.upload_progress.enabled] = off

Confirm the script still reports 0 when a .user.ini in the document root sets the value to 1. If the script reports 1, the pool used php_flag, or another per-directory file is winning. ini_set will not settle the question. The access mode is per-directory, and the call returns false.

After the file change, restart the workers and request again. A rewrite that is visible in the file and invisible in the next request means the old child is still in the pool. Recycle it, then read the script output a second time. Do not stop at the file checksum.

On Windows, compare the package name with PHP_VERSION. The 26 October NTS x64 VS16 zip for the 8.3 line is php-8.3.36-1-nts-Win32-vs16-x64.zip, and its published SHA-256 is the digest above. PHP_VERSION inside that package is still 8.3.36. In the official bookworm image, the matching questions are php -v and the installed libbrotli1 and libcurl4 versions. Those two package versions were identical on the 8.3.35 and 8.3.36 images tested here.

The handler switch is still available on the patched CLI. A configuration that starts on php_serialize or php_binary, and calls ini_set to php before session_start, is the shape named on 23 October. Leave upload progress off until the later bugfix if that shape is present. Applications that never change the handler are outside that note. They are still inside the 21 October advisory until the binary serving traffic is the patched release.

None of these checks replace the update. They tell you whether the running worker is the binary you think it is, and whether the setting PHP asked you to keep off is off in that worker.

Sources