1
0
mirror of https://github.com/php/php-src.git synced 2026-03-24 00:02:20 +01:00

Fix GH-17645: FPM with httpd ProxyPass does not decode script path

This changes make FPM always decode SCRIPT_FILENAME when Apache
ProxyPass or ProxyPassMatch is used. It also introduces a new INI
option fastcgi.script_path_encoded that allows using the previous
behavior of not decoding the path. The INI is introduced because
there is a chance that some users could use encoded file paths in
their file system as a workaround for the previous behavior.

Close GH-17896
This commit is contained in:
Jakub Zelenka
2025-02-22 22:59:07 +01:00
parent e1845365ea
commit 5ff8d6d0d2
8 changed files with 160 additions and 13 deletions

4
NEWS
View File

@@ -62,6 +62,10 @@ PHP NEWS
- Fileinfo:
. Upgrade to file 5.46. (nielsdos)
- FPM:
. Fixed GH-17645 (FPM with httpd ProxyPass does not decode script path).
(Jakub Zelenka)
- GD:
. Fixed bug #68629 (Transparent artifacts when using imagerotate). (pierre,
cmb)

View File

@@ -152,6 +152,10 @@ PHP 8.5 UPGRADE NOTES
. Added a new --ini=diff option to print INI settings changed from the builtin
default.
- FPM:
. FPM with httpd ProxyPass decodes the full scirpt path script path. Added
fastcgi.script_path_encoded INI setting to prevent this new behevior.
========================================
4. Deprecated Functionality
========================================

View File

@@ -817,6 +817,12 @@ enable_dl = Off
; https://php.net/fastcgi.impersonate
;fastcgi.impersonate = 1
; Prevent decoding of SCRIPT_FILENAME when using Apache ProxyPass or
; ProxyPassMatch. This should only be used if script file paths are already
; stored in an encoded format on the file system.
; Default is 0.
;fastcgi.script_path_encoded = 1
; Disable logging through FastCGI connection. PHP's default behavior is to enable
; this feature.
;fastcgi.logging = 0

View File

@@ -819,6 +819,12 @@ enable_dl = Off
; https://php.net/fastcgi.impersonate
;fastcgi.impersonate = 1
; Prevent decoding of SCRIPT_FILENAME when using Apache ProxyPass or
; ProxyPassMatch. This should only be used if script file paths are already
; stored in an encoded format on the file system.
; Default is 0.
;fastcgi.script_path_encoded = 1
; Disable logging through FastCGI connection. PHP's default behavior is to enable
; this feature.
;fastcgi.logging = 0

View File

@@ -144,6 +144,7 @@ typedef struct _php_cgi_globals_struct {
bool nph;
bool fix_pathinfo;
bool discard_path;
bool fcgi_script_path_encoded;
bool fcgi_logging;
bool fcgi_logging_request_started;
HashTable user_config_cache;
@@ -1066,6 +1067,10 @@ static void init_request_info(void)
}
}
if (apache_was_here && !CGIG(fcgi_script_path_encoded)) {
php_raw_url_decode(env_script_filename, strlen(env_script_filename));
}
if (CGIG(fix_pathinfo)) {
struct stat st;
char *real_path = NULL;
@@ -1174,7 +1179,7 @@ static void init_request_info(void)
* it is probably also in SCRIPT_NAME and need to be removed
*/
size_t decoded_path_info_len = 0;
if (strchr(path_info, '%')) {
if (CGIG(fcgi_script_path_encoded) && strchr(path_info, '%')) {
decoded_path_info = estrdup(path_info);
decoded_path_info_len = php_raw_url_decode(decoded_path_info, strlen(path_info));
}
@@ -1421,13 +1426,14 @@ static void fastcgi_ini_parser(zval *arg1, zval *arg2, zval *arg3, int callback_
/* }}} */
PHP_INI_BEGIN()
STD_PHP_INI_BOOLEAN("cgi.rfc2616_headers", "0", PHP_INI_ALL, OnUpdateBool, rfc2616_headers, php_cgi_globals_struct, php_cgi_globals)
STD_PHP_INI_BOOLEAN("cgi.nph", "0", PHP_INI_ALL, OnUpdateBool, nph, php_cgi_globals_struct, php_cgi_globals)
STD_PHP_INI_BOOLEAN("cgi.fix_pathinfo", "1", PHP_INI_SYSTEM, OnUpdateBool, fix_pathinfo, php_cgi_globals_struct, php_cgi_globals)
STD_PHP_INI_BOOLEAN("cgi.discard_path", "0", PHP_INI_SYSTEM, OnUpdateBool, discard_path, php_cgi_globals_struct, php_cgi_globals)
STD_PHP_INI_BOOLEAN("fastcgi.logging", "1", PHP_INI_SYSTEM, OnUpdateBool, fcgi_logging, php_cgi_globals_struct, php_cgi_globals)
STD_PHP_INI_ENTRY("fastcgi.error_header", NULL, PHP_INI_SYSTEM, OnUpdateString, error_header, php_cgi_globals_struct, php_cgi_globals)
STD_PHP_INI_ENTRY("fpm.config", NULL, PHP_INI_SYSTEM, OnUpdateString, fpm_config, php_cgi_globals_struct, php_cgi_globals)
STD_PHP_INI_BOOLEAN("cgi.rfc2616_headers", "0", PHP_INI_ALL, OnUpdateBool, rfc2616_headers, php_cgi_globals_struct, php_cgi_globals)
STD_PHP_INI_BOOLEAN("cgi.nph", "0", PHP_INI_ALL, OnUpdateBool, nph, php_cgi_globals_struct, php_cgi_globals)
STD_PHP_INI_BOOLEAN("cgi.fix_pathinfo", "1", PHP_INI_SYSTEM, OnUpdateBool, fix_pathinfo, php_cgi_globals_struct, php_cgi_globals)
STD_PHP_INI_BOOLEAN("cgi.discard_path", "0", PHP_INI_SYSTEM, OnUpdateBool, discard_path, php_cgi_globals_struct, php_cgi_globals)
STD_PHP_INI_BOOLEAN("fastcgi.script_path_encoded", "0", PHP_INI_SYSTEM, OnUpdateBool, fcgi_script_path_encoded, php_cgi_globals_struct, php_cgi_globals)
STD_PHP_INI_BOOLEAN("fastcgi.logging", "1", PHP_INI_SYSTEM, OnUpdateBool, fcgi_logging, php_cgi_globals_struct, php_cgi_globals)
STD_PHP_INI_ENTRY("fastcgi.error_header", NULL, PHP_INI_SYSTEM, OnUpdateString, error_header, php_cgi_globals_struct, php_cgi_globals)
STD_PHP_INI_ENTRY("fpm.config", NULL, PHP_INI_SYSTEM, OnUpdateString, fpm_config, php_cgi_globals_struct, php_cgi_globals)
PHP_INI_END()
/* {{{ php_cgi_globals_ctor */
@@ -1437,6 +1443,7 @@ static void php_cgi_globals_ctor(php_cgi_globals_struct *php_cgi_globals)
php_cgi_globals->nph = 0;
php_cgi_globals->fix_pathinfo = 1;
php_cgi_globals->discard_path = 0;
php_cgi_globals->fcgi_script_path_encoded = 0;
php_cgi_globals->fcgi_logging = 1;
php_cgi_globals->fcgi_logging_request_started = false;
zend_hash_init(&php_cgi_globals->user_config_cache, 0, NULL, user_config_cache_entry_dtor, 1);

View File

@@ -0,0 +1,54 @@
--TEST--
FPM: FastCGI change for Apache ProxyPass SCRIPT_FILENAME decoding (GH-17645)
--SKIPIF--
<?php include "skipif.inc"; ?>
--FILE--
<?php
require_once "tester.inc";
$cfg = <<<EOT
[global]
error_log = {{FILE:LOG}}
[unconfined]
listen = {{ADDR}}
pm = dynamic
pm.max_children = 5
pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = 3
php_admin_value[cgi.fix_pathinfo] = yes
EOT;
$code = <<<EOT
<?php
echo \$_SERVER["SCRIPT_NAME"] . "\n";
echo \$_SERVER["ORIG_SCRIPT_NAME"] . "\n";
echo \$_SERVER["SCRIPT_FILENAME"] . "\n";
echo \$_SERVER["PATH_INFO"] . "\n";
echo \$_SERVER["PHP_SELF"];
EOT;
$tester = new FPM\Tester($cfg, $code);
[$sourceFilePath, $scriptName] = $tester->createSourceFileAndScriptName('+');
$tester->start();
$tester->expectLogStartNotices();
$tester
->request(
uri: $scriptName . '/1%202',
scriptFilename: "proxy:fcgi://" . $tester->getAddr() . str_replace('+', '%2B', $sourceFilePath) . '/1%202',
scriptName: $scriptName . '/1 2'
)
->expectBody([$scriptName, $scriptName . '/1 2', $sourceFilePath, '/1 2', $scriptName . '/1 2']);
$tester->terminate();
$tester->close();
?>
Done
--EXPECT--
Done
--CLEAN--
<?php
require_once "tester.inc";
FPM\Tester::clean();
?>

View File

@@ -0,0 +1,55 @@
--TEST--
FPM: FastCGI change for Apache ProxyPass SCRIPT_FILENAME decoding - fallback (GH-17645)
--SKIPIF--
<?php include "skipif.inc"; ?>
--FILE--
<?php
require_once "tester.inc";
$cfg = <<<EOT
[global]
error_log = {{FILE:LOG}}
[unconfined]
listen = {{ADDR}}
pm = dynamic
pm.max_children = 5
pm.start_servers = 1
pm.min_spare_servers = 1
pm.max_spare_servers = 3
php_admin_value[cgi.fix_pathinfo] = yes
php_admin_value[fastcgi.script_path_encoded] = yes
EOT;
$code = <<<EOT
<?php
echo \$_SERVER["SCRIPT_NAME"] . "\n";
echo \$_SERVER["ORIG_SCRIPT_NAME"] . "\n";
echo \$_SERVER["SCRIPT_FILENAME"] . "\n";
echo \$_SERVER["PATH_INFO"] . "\n";
echo \$_SERVER["PHP_SELF"];
EOT;
$tester = new FPM\Tester($cfg, $code);
[$sourceFilePath, $scriptName] = $tester->createSourceFileAndScriptName('%2B');
$tester->start();
$tester->expectLogStartNotices();
$tester
->request(
uri: $scriptName . '/1%202',
scriptFilename: "proxy:fcgi://" . $tester->getAddr() . $sourceFilePath . '/1%202',
scriptName: $scriptName . '/1 2'
)
->expectBody([$scriptName, $scriptName . '/1 2', $sourceFilePath, '/1 2', $scriptName . '/1 2']);
$tester->terminate();
$tester->close();
?>
Done
--EXPECT--
Done
--CLEAN--
<?php
require_once "tester.inc";
FPM\Tester::clean();
?>

View File

@@ -47,7 +47,7 @@ class Tester
self::FILE_EXT_LOG_ERR,
self::FILE_EXT_LOG_SLOW,
self::FILE_EXT_PID,
'src.php',
'*src.php',
'ini',
'skip.ini',
'*.sock',
@@ -107,6 +107,11 @@ class Tester
*/
private string $fileName;
/**
* @var string
*/
private ?string $sourceFile = null;
/**
* @var resource
*/
@@ -1483,21 +1488,27 @@ class Tester
/**
* Create a source code file.
*
* @param string $nameSuffix
* @return string
*/
public function makeSourceFile(): string
public function makeSourceFile(string $nameSuffix = ''): string
{
return $this->makeFile('src.php', $this->code, overwrite: false);
if (is_null($this->sourceFile)) {
$this->sourceFile = $this->makeFile($nameSuffix . 'src.php', $this->code, overwrite: false);
}
return $this->sourceFile;
}
/**
* Create a source file and script name.
*
* @param string $nameSuffix
* @return string[]
*/
public function createSourceFileAndScriptName(): array
public function createSourceFileAndScriptName(string $nameSuffix = ''): array
{
$sourceFile = $this->makeFile('src.php', $this->code, overwrite: false);
$sourceFile = $this->makeSourceFile($nameSuffix);
return [$sourceFile, '/' . basename($sourceFile)];
}