@@ -164,6 +164,7 @@ static const opt_struct OPTIONS[] = {
164164typedef struct _php_cgi_globals_struct {
165165 HashTable user_config_cache ;
166166 char * redirect_status_env ;
167+ char * security_limit_extensions ;
167168 bool rfc2616_headers ;
168169 bool nph ;
169170 bool check_shebang_line ;
@@ -1505,6 +1506,7 @@ PHP_INI_BEGIN()
15051506 STD_PHP_INI_BOOLEAN ("cgi.check_shebang_line" , "1" , PHP_INI_SYSTEM , OnUpdateBool , check_shebang_line , php_cgi_globals_struct , php_cgi_globals )
15061507 STD_PHP_INI_BOOLEAN ("cgi.force_redirect" , "1" , PHP_INI_SYSTEM , OnUpdateBool , force_redirect , php_cgi_globals_struct , php_cgi_globals )
15071508 STD_PHP_INI_ENTRY ("cgi.redirect_status_env" , NULL , PHP_INI_SYSTEM , OnUpdateString , redirect_status_env , php_cgi_globals_struct , php_cgi_globals )
1509+ STD_PHP_INI_ENTRY ("cgi.security_limit_extensions" , ".php .phar" , PHP_INI_PERDIR , OnUpdateString , security_limit_extensions , php_cgi_globals_struct , php_cgi_globals )
15081510 STD_PHP_INI_BOOLEAN ("cgi.fix_pathinfo" , "1" , PHP_INI_SYSTEM , OnUpdateBool , fix_pathinfo , php_cgi_globals_struct , php_cgi_globals )
15091511 STD_PHP_INI_BOOLEAN ("cgi.discard_path" , "0" , PHP_INI_SYSTEM , OnUpdateBool , discard_path , php_cgi_globals_struct , php_cgi_globals )
15101512 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)
15241526 php_cgi_globals_ptr -> check_shebang_line = 1 ;
15251527 php_cgi_globals_ptr -> force_redirect = 1 ;
15261528 php_cgi_globals_ptr -> redirect_status_env = NULL ;
1529+ php_cgi_globals_ptr -> security_limit_extensions = NULL ;
15271530 php_cgi_globals_ptr -> fix_pathinfo = 1 ;
15281531 php_cgi_globals_ptr -> discard_path = 0 ;
15291532 php_cgi_globals_ptr -> fcgi_logging = 1 ;
@@ -1684,6 +1687,43 @@ static void add_response_header(sapi_header_struct *h, zval *return_value) /* {{
16841687}
16851688/* }}} */
16861689
1690+ bool cgi_extension_is_allowed (char * path ) {
1691+ const char * allowed_extensions = CGIG (security_limit_extensions );
1692+ if (!path || !allowed_extensions || strlen (allowed_extensions ) == 0 ) {
1693+ // No path, or no filter configured
1694+ return true;
1695+ }
1696+
1697+ const char * dot = strrchr (path , '.' );
1698+ if (!dot ) {
1699+ // No file extension
1700+ return false;
1701+ }
1702+
1703+ /* We want to be able to allow things like `.test.php` (but not all of
1704+ * `.php` or `.test`. For each extension in the list, check if the path
1705+ * given ends with exactly that. Allowing `.php` allows `.test.php`,
1706+ * `.foo.php`, and everything else because dots are valid file name
1707+ * characters. */
1708+ const char * next = allowed_extensions ;
1709+ const size_t path_len = strlen (path );
1710+ while (next && * next ) {
1711+ size_t extension_len = strcspn (next , " \t" );
1712+
1713+ if (extension_len <= path_len ) {
1714+ if (memcmp (path + path_len - extension_len , next , extension_len ) == 0 ) {
1715+ return true;
1716+ }
1717+ }
1718+ next += extension_len ;
1719+ if (* next ) {
1720+ next += strspn (next , " \t" );
1721+ }
1722+ }
1723+
1724+ return false;
1725+ }
1726+
16871727PHP_FUNCTION (apache_response_headers ) /* {{{ */
16881728{
16891729 ZEND_PARSE_PARAMETERS_NONE ();
@@ -2475,9 +2515,10 @@ consult the installation file that came with this distribution, or visit \n\
24752515 2. we are running as cgi or fastcgi
24762516 */
24772517 if (cgi || fastcgi || SG (request_info ).path_translated ) {
2478- if (php_fopen_primary_script (& file_handle ) == FAILURE ) {
2518+ bool limited_extension = !cgi_extension_is_allowed (SG (request_info ).path_translated );
2519+ if (limited_extension || php_fopen_primary_script (& file_handle ) == FAILURE ) {
24792520 zend_try {
2480- if (errno == EACCES ) {
2521+ if (limited_extension || errno == EACCES ) {
24812522 SG (sapi_headers ).http_response_code = 403 ;
24822523 PUTS ("Access denied.\n" );
24832524 } else {
0 commit comments