Repository navigation
PHP-FPM: PHP_VALUE settings persist across requests #24689
Description
Description
I haven't thoroughly tested the exact reproduction steps or conditions yet, but I have observed this behavior for years: settings passed via PHP_VALUE appear to persist throughout the lifetime of a PHP-FPM worker.
SetEnvIf ^ ^ "PHP_VALUE=post_max_size=40G;"
SetEnvIf ^ ^ "PHP_VALUE=upload_max_filesize=2G;"
This of course doesn't work; only the latter setting is supposed to work. But in reality, both post_max_size and upload_max_filesize are often set with this configuration.
Recently, I realized this could be a bug, as it seems I can actually place an .htaccess file anywhere in the document root with "PHP_VALUE=post_max_size=100G", and then post_max_size becomes 100G even above the directory where I placed the .htaccess file, if the request is handled by the same PHP-FPM worker.
The issue seems to be that once a PHP-FPM worker receives an .htaccess-originated PHP_VALUE, it doesn't clear the value when it finishes processing the request, and the next request for any page handled by the same worker uses the same value.
Expected behavior
Each request should start from the PHP/FPM configuration baseline.
A PHP_VALUE override supplied for request A should affect request A only.
If request B does not contain that override, request B should see the
configured default/pool value.
Actual behavior
For example:
Request A:
PHP_VALUE=upload_max_filesize=2G
The request reports:
upload_max_filesize=2G
Request B is then handled by the same FPM worker without a PHP_VALUE
override for upload_max_filesize.
Request B still reports:
upload_max_filesize=2G
instead of the configured baseline value.
The behavior is reproducible when both requests are handled by the same
FPM worker.
Reproduction
PHP script:
<?php
echo "pid=", getmypid(), "\n";
echo "upload_max_filesize=", ini_get('upload_max_filesize'), "\n";
echo "post_max_size=", ini_get('post_max_size'), "\n";
- Send a request with PHP_VALUE=upload_max_filesize=2G.
- Confirm that the override is applied.
- Send another request handled by the same FPM worker without that
override. - Observe that the previous value remains active.
Environment
- PHP: 8.5.12
- SAPI: FPM/FastCGI
- OS: macOS
- PHP built from source
Investigation
The FastCGI INI parser applies PHP_VALUE / PHP_ADMIN_VALUE through
fpm_php_apply_defines_ex().
That path ultimately modifies the zend_ini_entry through the FPM master
INI alteration path.
In contrast, request-scoped INI changes made through the normal Zend INI
alteration API are registered in EG(modified_ini_directives), preserving
the original value so that request deactivation can restore it.
This may explain why an FPM FastCGI override can survive request
deactivation, but further investigation is needed to determine the correct
fix without changing the semantics of FPM pool-level php_value and
php_admin_value settings.
PHP Version
PHP 8.5.12
Operating System
macOS
Activity
ultravistor commentedon Oct 25, 2026
Below is a rough patch for this issue.
fastcgi_ini_parser() in sapi/fpm/fpm/fpm_main.c
This is only a rough patch and may need further changes.
srayuth089 commentedon Oct 25, 2026
This observation connects to the "serialize-handler mismatch" path documented in the php2shell patch-gap research (dinosn/php-cve-2026-10961x-lab-poc, finding #2, 2026-10-22).
The scenario there: if
PHP_VALUE(or.user.ini) can setsession.serialize_handlerfor one request, and the setting persists in the FPM worker as you describe, then:ini_set('session.serialize_handler', 'php_serialize')+ session writephphandler decodes the session written byphp_serialize|O:payload in the session data triggers__destructduringvar_hashcleanupThe persistence you've observed (
PHP_VALUEsurviving across requests in the same worker) is what makes this cross-request attack feasible — without persistence, each request would start with a clean handler.This was tested on PHP 8.4.26-FPM and confirmed to work when the handler divergence is set up (either via
php_valuein pool config or.user.iniper-directory). The patch'sincomplete_classes_onlyguard (3584a98) is scoped to the pre-RINIT upload-progress window (PS(in_rfc1867_update)), not to app-phasesession_start()decode — so this path survives the 2026-10 security patch.If
PHP_VALUEpersistence is intentional FPM behavior (performance optimization to avoid re-parsing pool config), it might be worth documenting this interaction as a known security consideration for multi-tenant FPM setups.ultravistor commentedon Oct 25, 2026
Thanks, I just tested session.serialize_handler and it looks like the same persistence occurs.
SetEnvIf ^ ^ "PHP_VALUE=session.serialize_handler=php_serialize;"# SetEnvIf ^ ^ "PHP_VALUE=session.serialize_handler=php_serialize;"While the default value appears to be
php, when both requests are handled by the same PHP-FPM worker, ini_get('session.serialize_handler') reportedphp_serializein Request B using the test page below.So the issue is not limited to settings such as post_max_size or upload_max_filesize (it also affects session.serialize_handler). I haven't fully tested the exploit scenario described above yet. But thanks for your response.