diff --git a/ext/dl_test/tests/frameless_temporary.phpt b/ext/dl_test/tests/frameless_temporary.phpt index fe38123d99e8..2073aabdbf46 100644 --- a/ext/dl_test/tests/frameless_temporary.phpt +++ b/ext/dl_test/tests/frameless_temporary.phpt @@ -29,6 +29,7 @@ $cmd = 'env -u SCRIPT_FILENAME -u PATH_TRANSLATED -u REDIRECT_STATUS -u REQUEST_ . ' setarch ' . escapeshellarg(php_uname('m')) . ' -R ' . escapeshellarg(getenv('TEST_PHP_CGI_EXECUTABLE')) . ' -n -q -T 2 -d enable_dl=1 -d extension_dir=' . escapeshellarg(ini_get('extension_dir')) + . ' -d cgi.security_limit_extensions=.inc' . ' ' . escapeshellarg(__DIR__ . '/frameless_temporary_cgi.inc'); $proc = proc_open($cmd, [ 0 => ['pipe', 'r'], diff --git a/sapi/cgi/cgi_main.c b/sapi/cgi/cgi_main.c index 929c3db42733..e7c7deab0cb8 100644 --- a/sapi/cgi/cgi_main.c +++ b/sapi/cgi/cgi_main.c @@ -164,6 +164,7 @@ static const opt_struct OPTIONS[] = { typedef struct _php_cgi_globals_struct { HashTable user_config_cache; char *redirect_status_env; + char *security_limit_extensions; bool rfc2616_headers; bool nph; bool check_shebang_line; @@ -1505,6 +1506,7 @@ PHP_INI_BEGIN() STD_PHP_INI_BOOLEAN("cgi.check_shebang_line", "1", PHP_INI_SYSTEM, OnUpdateBool, check_shebang_line, php_cgi_globals_struct, php_cgi_globals) STD_PHP_INI_BOOLEAN("cgi.force_redirect", "1", PHP_INI_SYSTEM, OnUpdateBool, force_redirect, php_cgi_globals_struct, php_cgi_globals) STD_PHP_INI_ENTRY("cgi.redirect_status_env", NULL, PHP_INI_SYSTEM, OnUpdateString, redirect_status_env, php_cgi_globals_struct, php_cgi_globals) + STD_PHP_INI_ENTRY("cgi.security_limit_extensions", ".php .phar", PHP_INI_PERDIR, OnUpdateString, security_limit_extensions, 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) @@ -1524,6 +1526,7 @@ static void php_cgi_globals_ctor(php_cgi_globals_struct *php_cgi_globals_ptr) php_cgi_globals_ptr->check_shebang_line = 1; php_cgi_globals_ptr->force_redirect = 1; php_cgi_globals_ptr->redirect_status_env = NULL; + php_cgi_globals_ptr->security_limit_extensions = NULL; php_cgi_globals_ptr->fix_pathinfo = 1; php_cgi_globals_ptr->discard_path = 0; php_cgi_globals_ptr->fcgi_logging = 1; @@ -1684,6 +1687,43 @@ static void add_response_header(sapi_header_struct *h, zval *return_value) /* {{ } /* }}} */ +bool cgi_extension_is_allowed(char *path) { + const char *allowed_extensions = CGIG(security_limit_extensions); + if (!path || !allowed_extensions || strlen(allowed_extensions) == 0) { + // No path, or no filter configured + return true; + } + + const char *dot = strrchr(path, '.'); + if (!dot) { + // No file extension + return false; + } + + /* We want to be able to allow things like `.test.php` (but not all of + * `.php` or `.test`. For each extension in the list, check if the path + * given ends with exactly that. Allowing `.php` allows `.test.php`, + * `.foo.php`, and everything else because dots are valid file name + * characters. */ + const char *next = allowed_extensions; + const size_t path_len = strlen(path); + while (next && *next) { + size_t extension_len = strcspn(next, " \t"); + + if (extension_len <= path_len) { + if (memcmp(path + path_len - extension_len, next, extension_len) == 0) { + return true; + } + } + next += extension_len; + if (*next) { + next += strspn(next, " \t"); + } + } + + return false; +} + PHP_FUNCTION(apache_response_headers) /* {{{ */ { ZEND_PARSE_PARAMETERS_NONE(); @@ -2475,9 +2515,10 @@ consult the installation file that came with this distribution, or visit \n\ 2. we are running as cgi or fastcgi */ if (cgi || fastcgi || SG(request_info).path_translated) { - if (php_fopen_primary_script(&file_handle) == FAILURE) { + bool limited_extension = !cgi_extension_is_allowed(SG(request_info).path_translated); + if (limited_extension || php_fopen_primary_script(&file_handle) == FAILURE) { zend_try { - if (errno == EACCES) { + if (limited_extension || errno == EACCES) { SG(sapi_headers).http_response_code = 403; PUTS("Access denied.\n"); } else { diff --git a/sapi/cgi/tests/003.phpt b/sapi/cgi/tests/003.phpt index 81d46e868b3d..4d6a6b27822b 100644 --- a/sapi/cgi/tests/003.phpt +++ b/sapi/cgi/tests/003.phpt @@ -38,10 +38,10 @@ class test { /* {{{ */ file_put_contents($filename, $code); var_dump(shell_exec(<<" | $php -n -w diff --git a/sapi/cgi/tests/004.phpt b/sapi/cgi/tests/004.phpt index fddc5ee80edb..0afce8000e16 100644 --- a/sapi/cgi/tests/004.phpt +++ b/sapi/cgi/tests/004.phpt @@ -28,15 +28,15 @@ file_put_contents($filename, $code); if (defined("PHP_WINDOWS_VERSION_MAJOR")) { var_dump(shell_exec(<</dev/null + $php -n -d cgi.security_limit_extensions='.test.php' -f "$filename" 2>/dev/null SHELL)); } var_dump(shell_exec(<<'); -echo shell_exec("$php -n $f"); +echo shell_exec("$php -n -d cgi.security_limit_extensions='' $f"); echo "Done\n"; diff --git a/sapi/cgi/tests/010.phpt b/sapi/cgi/tests/010.phpt index 2f5d9a1356d3..769661ed1f98 100644 --- a/sapi/cgi/tests/010.phpt +++ b/sapi/cgi/tests/010.phpt @@ -19,19 +19,19 @@ header("HTTP/1.1 403 Forbidden"); header("Status: 403 Also Forbidden"); ?>'); -echo shell_exec("$php -n $f"); +echo shell_exec("$php -n -d cgi.security_limit_extensions='' $f"); file_put_contents($f, ''); -echo shell_exec("$php -n $f"); +echo shell_exec("$php -n -d cgi.security_limit_extensions='' $f"); file_put_contents($f, ''); -echo shell_exec("$php -n $f"); +echo shell_exec("$php -n -d cgi.security_limit_extensions='' $f"); echo "Done\n"; diff --git a/sapi/cgi/tests/011.phpt b/sapi/cgi/tests/011.phpt index 263c2e61ef81..a9759a02b6c1 100644 --- a/sapi/cgi/tests/011.phpt +++ b/sapi/cgi/tests/011.phpt @@ -15,7 +15,7 @@ $f = tempnam(sys_get_temp_dir(), 'cgitest'); function test($script) { file_put_contents($GLOBALS['f'], $script); $cmd = escapeshellcmd($GLOBALS['php']); - $cmd .= ' -n -dreport_zend_debug=0 -dhtml_errors=0 ' . escapeshellarg($GLOBALS['f']); + $cmd .= ' -n -dreport_zend_debug=0 -dhtml_errors=0 -dcgi.security_limit_extensions="" ' . escapeshellarg($GLOBALS['f']); echo "----------\n"; echo rtrim($script) . "\n"; echo "----------\n"; diff --git a/sapi/cgi/tests/012.phpt b/sapi/cgi/tests/012.phpt index 7f981b7da8d0..bb3d641c53c0 100644 --- a/sapi/cgi/tests/012.phpt +++ b/sapi/cgi/tests/012.phpt @@ -49,12 +49,13 @@ class test file_put_contents($filename_bad, $code); -run_and_output("$php -n -l $filename_good_escaped $filename_good_escaped"); -run_and_output("$php -n -l $filename_good_escaped some.unknown $filename_good_escaped"); -run_and_output("$php -n -l $filename_good_escaped $filename_bad_escaped $filename_good_escaped"); -run_and_output("$php -n -l $filename_bad_escaped $filename_bad_escaped"); -run_and_output("$php -n -l $filename_bad_escaped some.unknown $filename_bad_escaped"); -run_and_output("$php -n -l $filename_bad_escaped $filename_bad_escaped some.unknown"); +$cmd = "$php -n -d cgi.security_limit_extensions=" . escapeshellarg('.test.php .unknown'); +run_and_output("$cmd -l $filename_good_escaped $filename_good_escaped"); +run_and_output("$cmd -l $filename_good_escaped some.unknown $filename_good_escaped"); +run_and_output("$cmd -l $filename_good_escaped $filename_bad_escaped $filename_good_escaped"); +run_and_output("$cmd -l $filename_bad_escaped $filename_bad_escaped"); +run_and_output("$cmd -l $filename_bad_escaped some.unknown $filename_bad_escaped"); +run_and_output("$cmd -l $filename_bad_escaped $filename_bad_escaped some.unknown"); echo "Done\n"; ?> diff --git a/sapi/cgi/tests/bug75574_utf8.phpt b/sapi/cgi/tests/bug75574_utf8.phpt index 76d2d8c1f4cd..7c7eac1545ab 100644 --- a/sapi/cgi/tests/bug75574_utf8.phpt +++ b/sapi/cgi/tests/bug75574_utf8.phpt @@ -19,7 +19,7 @@ reset_env_vars(); $fn = __DIR__ . DIRECTORY_SEPARATOR . md5(uniqid()); file_put_contents($fn, " +--INI-- +display_errors=stdout +--FILE-- + +--EXPECTF-- +Default: allowed are .php and .phar: +%ssecurity_limit_extensions.php, ini=: +No syntax errors detected in %ssecurity_limit_extensions.php + +%smissing.test.php, ini=: +No input file specified. + +%smissing.phar, ini=: +No input file specified. + +%sinclude.inc, ini=: +Access denied. + + + +Configured extensions: +%ssecurity_limit_extensions.php, ini=-d cgi.security_limit_extensions='.inc .phar .php': +No syntax errors detected in %ssecurity_limit_extensions.php + +%ssecurity_limit_extensions.php, ini=-d cgi.security_limit_extensions='.inc .phar .php': +No syntax errors detected in %ssecurity_limit_extensions.php + + + +Multiple parts to extension: +%ssecurity_limit_extensions.php, ini=-d cgi.security_limit_extensions='.test.php': +Access denied. + +%smissing.test.php, ini=-d cgi.security_limit_extensions='.test.php': +No input file specified. + + + +Allowed longer than path, then .php: +%ssecurity_limit_extensions.php, ini=-d cgi.security_limit_extensions='.x%rx+%ryz .php': +No syntax errors detected in %ssecurity_limit_extensions.php diff --git a/tests/basic/GHSA-9pqp-7h25-4f32.phpt b/tests/basic/GHSA-9pqp-7h25-4f32.phpt index 17a145ad9d0d..84f650f57545 100644 --- a/tests/basic/GHSA-9pqp-7h25-4f32.phpt +++ b/tests/basic/GHSA-9pqp-7h25-4f32.phpt @@ -21,6 +21,8 @@ function test($boundaryLen) { getenv('TEST_PHP_CGI_EXECUTABLE'), '-C', '-n', + '-d', + 'cgi.security_limit_extensions=".inc"', __DIR__ . '/GHSA-9pqp-7h25-4f32.inc', ]; diff --git a/tests/basic/gh16998.phpt b/tests/basic/gh16998.phpt index 8bfcbbda99dd..b6097c363560 100644 --- a/tests/basic/gh16998.phpt +++ b/tests/basic/gh16998.phpt @@ -13,6 +13,8 @@ $cmd = [ getenv('TEST_PHP_CGI_EXECUTABLE'), '-C', '-n', + '-d', + 'cgi.security_limit_extensions=".inc"', __DIR__ . '/GHSA-9pqp-7h25-4f32.inc', ]; $boundary = str_repeat('A', FILLUNIT);