Skip to content

Commit

Permalink
Fix phpGH-17645: FPM with httpd ProxyPass does not decode script path
Browse files Browse the repository at this point in the history
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 phpGH-17896
  • Loading branch information
bukka committed Mar 2, 2025
1 parent 3677871 commit f990379
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 13 deletions.
6 changes: 6 additions & 0 deletions php.ini-development
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions php.ini-production
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 15 additions & 8 deletions sapi/fpm/fpm/fpm_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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));
}
Expand Down Expand Up @@ -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 */
Expand All @@ -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);
Expand Down
54 changes: 54 additions & 0 deletions sapi/fpm/tests/fcgi-env-pif-apache-pp-sfp-decoding.phpt
Original file line number Diff line number Diff line change
@@ -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();
?>
55 changes: 55 additions & 0 deletions sapi/fpm/tests/fcgi-env-pif-apache-pp-sfp-encoded.phpt
Original file line number Diff line number Diff line change
@@ -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();
?>
21 changes: 16 additions & 5 deletions sapi/fpm/tests/tester.inc
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -107,6 +107,11 @@ class Tester
*/
private string $fileName;

/**
* @var string
*/
private ?string $sourceFile = null;

/**
* @var resource
*/
Expand Down Expand Up @@ -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)];
}
Expand Down

0 comments on commit f990379

Please # to comment.