diff --git a/NEWS b/NEWS index f9b8fb276472d..e3e7a5613be3a 100644 --- a/NEWS +++ b/NEWS @@ -1,8 +1,45 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| -?? ??? ????, PHP 8.1.29 +26 Sep 2024, PHP 8.1.30 +- CGI: + . Fixed bug GHSA-p99j-rfp4-xqvq (Bypass of CVE-2024-4577, Parameter Injection + Vulnerability). (CVE-2024-8926) (nielsdos) + . Fixed bug GHSA-94p6-54jq-9mwp (cgi.force_redirect configuration is + bypassable due to the environment variable collision). (CVE-2024-8927) + (nielsdos) + +- FPM: + . Fixed bug GHSA-865w-9rf3-2wh5 (Logs from childrens may be altered). + (CVE-2024-9026) (Jakub Zelenka) +- SAPI: + . Fixed bug GHSA-9pqp-7h25-4f32 (Erroneous parsing of multipart form data). + (CVE-2024-8925) (Arnaud) + +06 Jun 2024, PHP 8.1.29 + +- CGI: + . Fixed bug GHSA-3qgc-jrrr-25jv (Bypass of CVE-2012-1823, Argument Injection + in PHP-CGI). (CVE-2024-4577) (nielsdos) + +- Filter: + . Fixed bug GHSA-w8qr-v226-r27w (Filter bypass in filter_var FILTER_VALIDATE_URL). + (CVE-2024-5458) (nielsdos) + +- OpenSSL: + . The openssl_private_decrypt function in PHP, when using PKCS1 padding + (OPENSSL_PKCS1_PADDING, which is the default), is vulnerable to the Marvin Attack + unless it is used with an OpenSSL version that includes the changes from this pull + request: https://github.com/openssl/openssl/pull/13817 (rsa_pkcs1_implicit_rejection). + These changes are part of OpenSSL 3.2 and have also been backported to stable + versions of various Linux distributions, as well as to the PHP builds provided for + Windows since the previous release. All distributors and builders should ensure that + this version is used to prevent PHP from being vulnerable. (CVE-2024-2408) + +- Standard: + . Fixed bug GHSA-9fcc-425m-g385 (Bypass of CVE-2024-1874). + (CVE-2024-5585) (nielsdos) 11 Apr 2024, PHP 8.1.28 @@ -31,7 +68,7 @@ PHP NEWS - FPM: . Fixed bug GH-12705 (Segmentation fault in fpm_status_export_to_zval). (Patrick Prasse) - + - Intl: . Fixed bug GH-12635 (Test bug69398.phpt fails with ICU 74.1). (nielsdos) diff --git a/Zend/zend.h b/Zend/zend.h index 9a414960e2a87..c8bad6826e55c 100644 --- a/Zend/zend.h +++ b/Zend/zend.h @@ -20,7 +20,7 @@ #ifndef ZEND_H #define ZEND_H -#define ZEND_VERSION "4.1.29-dev" +#define ZEND_VERSION "4.1.30" #define ZEND_ENGINE_3 diff --git a/configure.ac b/configure.ac index 83d50445be205..58c290325493a 100644 --- a/configure.ac +++ b/configure.ac @@ -17,7 +17,7 @@ dnl Basic autoconf initialization, generation of config.nice. dnl ---------------------------------------------------------------------------- AC_PREREQ([2.68]) -AC_INIT([PHP],[8.1.29-dev],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) +AC_INIT([PHP],[8.1.30],[https://github.com/php/php-src/issues],[php],[https://www.php.net]) AC_CONFIG_SRCDIR([main/php_version.h]) AC_CONFIG_AUX_DIR([build]) AC_PRESERVE_HELP_ORDER diff --git a/ext/filter/logical_filters.c b/ext/filter/logical_filters.c index 182f2b3cd0448..3db0ef4090500 100644 --- a/ext/filter/logical_filters.c +++ b/ext/filter/logical_filters.c @@ -89,7 +89,7 @@ #define FORMAT_IPV4 4 #define FORMAT_IPV6 6 -static int _php_filter_validate_ipv6(char *str, size_t str_len, int ip[8]); +static int _php_filter_validate_ipv6(const char *str, size_t str_len, int ip[8]); static int php_filter_parse_int(const char *str, size_t str_len, zend_long *ret) { /* {{{ */ zend_long ctx_value; @@ -580,6 +580,14 @@ static int is_userinfo_valid(zend_string *str) return 1; } +static bool php_filter_is_valid_ipv6_hostname(const char *s, size_t l) +{ + const char *e = s + l; + const char *t = e - 1; + + return *s == '[' && *t == ']' && _php_filter_validate_ipv6(s + 1, l - 2, NULL); +} + void php_filter_validate_url(https://redirect.github.com/PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */ { php_url *url; @@ -600,7 +608,7 @@ void php_filter_validate_url(https://redirect.github.com/PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */ if (url->scheme != NULL && (zend_string_equals_literal_ci(url->scheme, "http") || zend_string_equals_literal_ci(url->scheme, "https"))) { - char *e, *s, *t; + const char *s; size_t l; if (url->host == NULL) { @@ -609,17 +617,14 @@ void php_filter_validate_url(https://redirect.github.com/PHP_INPUT_FILTER_PARAM_DECL) /* {{{ */ s = ZSTR_VAL(url->host); l = ZSTR_LEN(url->host); - e = s + l; - t = e - 1; - - /* An IPv6 enclosed by square brackets is a valid hostname */ - if (*s == '[' && *t == ']' && _php_filter_validate_ipv6((s + 1), l - 2, NULL)) { - php_url_free(url); - return; - } - // Validate domain - if (!_php_filter_validate_domain(ZSTR_VAL(url->host), l, FILTER_FLAG_HOSTNAME)) { + if ( + /* An IPv6 enclosed by square brackets is a valid hostname.*/ + !php_filter_is_valid_ipv6_hostname(s, l) && + /* Validate domain. + * This includes a loose check for an IPv4 address. */ + !_php_filter_validate_domain(ZSTR_VAL(url->host), l, FILTER_FLAG_HOSTNAME) + ) { php_url_free(url); RETURN_VALIDATION_FAILED } @@ -753,15 +758,15 @@ static int _php_filter_validate_ipv4(char *str, size_t str_len, int *ip) /* {{{ } /* }}} */ -static int _php_filter_validate_ipv6(char *str, size_t str_len, int ip[8]) /* {{{ */ +static int _php_filter_validate_ipv6(const char *str, size_t str_len, int ip[8]) /* {{{ */ { int compressed_pos = -1; int blocks = 0; int num, n, i; char *ipv4; - char *end; + const char *end; int ip4elm[4]; - char *s = str; + const char *s = str; if (!memchr(str, ':', str_len)) { return 0; diff --git a/ext/filter/tests/ghsa-w8qr-v226-r27w.phpt b/ext/filter/tests/ghsa-w8qr-v226-r27w.phpt new file mode 100644 index 0000000000000..0092408ee5ad6 --- /dev/null +++ b/ext/filter/tests/ghsa-w8qr-v226-r27w.phpt @@ -0,0 +1,41 @@ +--TEST-- +GHSA-w8qr-v226-r27w +--EXTENSIONS-- +filter +--FILE-- + +--EXPECT-- +--- These ones should fail --- +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +bool(false) +--- These ones should work --- +string(21) "http://test@127.0.0.1/" +string(50) "http://test@[2001:db8:3333:4444:5555:6666:102:304]/" +string(17) "http://test@[::1]/" diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c index 8aae54072650e..495af6cba35e5 100644 --- a/ext/standard/proc_open.c +++ b/ext/standard/proc_open.c @@ -546,48 +546,39 @@ static void append_win_escaped_arg(smart_str *str, zend_string *arg, bool is_cmd smart_str_appendc(str, '"'); } -static inline int stricmp_end(const char* suffix, const char* str) { - size_t suffix_len = strlen(suffix); - size_t str_len = strlen(str); +static bool is_executed_by_cmd(const char *prog_name, size_t prog_name_length) +{ + size_t out_len; + WCHAR long_name[MAX_PATH]; + WCHAR full_name[MAX_PATH]; + LPWSTR file_part = NULL; - if (suffix_len > str_len) { - return -1; /* Suffix is longer than string, cannot match. */ - } + wchar_t *prog_name_wide = php_win32_cp_conv_any_to_w(prog_name, prog_name_length, &out_len); - /* Compare the end of the string with the suffix, ignoring case. */ - return _stricmp(str + (str_len - suffix_len), suffix); -} + if (GetLongPathNameW(prog_name_wide, long_name, MAX_PATH) == 0) { + /* This can fail for example with ERROR_FILE_NOT_FOUND (short path resolution only works for existing files) + * in which case we'll pass the path verbatim to the FullPath transformation. */ + lstrcpynW(long_name, prog_name_wide, MAX_PATH); + } -static bool is_executed_by_cmd(const char *prog_name) -{ - /* If program name is cmd.exe, then return true. */ - if (_stricmp("cmd.exe", prog_name) == 0 || _stricmp("cmd", prog_name) == 0 - || stricmp_end("\\cmd.exe", prog_name) == 0 || stricmp_end("\\cmd", prog_name) == 0) { - return true; - } + free(prog_name_wide); + prog_name_wide = NULL; - /* Find the last occurrence of the directory separator (backslash or forward slash). */ - char *last_separator = strrchr(prog_name, '\\'); - char *last_separator_fwd = strrchr(prog_name, '/'); - if (last_separator_fwd && (!last_separator || last_separator < last_separator_fwd)) { - last_separator = last_separator_fwd; + if (GetFullPathNameW(long_name, MAX_PATH, full_name, &file_part) == 0 || file_part == NULL) { + return false; } - /* Find the last dot in the filename after the last directory separator. */ - char *extension = NULL; - if (last_separator != NULL) { - extension = strrchr(last_separator, '.'); + bool uses_cmd = false; + if (_wcsicmp(file_part, L"cmd.exe") == 0 || _wcsicmp(file_part, L"cmd") == 0) { + uses_cmd = true; } else { - extension = strrchr(prog_name, '.'); - } - - if (extension == NULL || extension == prog_name) { - /* No file extension found, it is not batch file. */ - return false; + const WCHAR *extension_dot = wcsrchr(file_part, L'.'); + if (extension_dot && (_wcsicmp(extension_dot, L".bat") == 0 || _wcsicmp(extension_dot, L".cmd") == 0)) { + uses_cmd = true; + } } - /* Check if the file extension is ".bat" or ".cmd" which is always executed by cmd.exe. */ - return _stricmp(extension, ".bat") == 0 || _stricmp(extension, ".cmd") == 0; + return uses_cmd; } static zend_string *create_win_command_from_args(HashTable *args) @@ -606,7 +597,7 @@ static zend_string *create_win_command_from_args(HashTable *args) } if (is_prog_name) { - is_cmd_execution = is_executed_by_cmd(ZSTR_VAL(arg_str)); + is_cmd_execution = is_executed_by_cmd(ZSTR_VAL(arg_str), ZSTR_LEN(arg_str)); } else { smart_str_appendc(&str, ' '); } diff --git a/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_001.phpt b/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_001.phpt new file mode 100644 index 0000000000000..2873210608497 --- /dev/null +++ b/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_001.phpt @@ -0,0 +1,56 @@ +--TEST-- +GHSA-9fcc-425m-g385 - bypass CVE-2024-1874 - batch file variation +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +'"%sghsa-9fcc-425m-g385_001.bat."' is not recognized as an internal or external command, +operable program or batch file. +%sghsa-9fcc-425m-g385_001.bat +"¬epad.exe +%sghsa-9fcc-425m-g385_001.bat. +"¬epad.exe +%sghsa-9fcc-425m-g385_001.bat. ... +"¬epad.exe +%sghsa-9fcc-425m-g385_001.bat. ... . +"¬epad.exe +'"%sghsa-9fcc-425m-g385_001.bat. ... . ."' is not recognized as an internal or external command, +operable program or batch file. + +Warning: proc_open(): CreateProcess failed, error code: 2 in %s on line %d +--CLEAN-- + diff --git a/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_002.phpt b/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_002.phpt new file mode 100644 index 0000000000000..714836557af5c --- /dev/null +++ b/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_002.phpt @@ -0,0 +1,66 @@ +--TEST-- +GHSA-9fcc-425m-g385 - bypass CVE-2024-1874 - cmd.exe variation +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +%sghsa-9fcc-425m-g385_002.bat +"¬epad.exe +%sghsa-9fcc-425m-g385_002.bat +"¬epad.exe +%sghsa-9fcc-425m-g385_002.bat +"¬epad.exe +%sghsa-9fcc-425m-g385_002.bat +"¬epad.exe + +Warning: proc_open(): CreateProcess failed, error code: 2 in %s on line %d +%sghsa-9fcc-425m-g385_002.bat +"¬epad.exe +%sghsa-9fcc-425m-g385_002.bat +"¬epad.exe + +Warning: proc_open(): CreateProcess failed, error code: 2 in %s on line %d + +Warning: proc_open(): CreateProcess failed, error code: 2 in %s on line %d + +Warning: proc_open(): CreateProcess failed, error code: 2 in %s on line %d +--CLEAN-- + diff --git a/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_003.phpt b/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_003.phpt new file mode 100644 index 0000000000000..a632965eb989a --- /dev/null +++ b/ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_003.phpt @@ -0,0 +1,550 @@ +--TEST-- +GHSA-9fcc-425m-g385 - bypass CVE-2024-1874 - exhaustive suffix test +--SKIPIF-- + +--FILE-- + true)); + var_dump($proc); + proc_close($proc); + } catch (Error) {} +} + +?> +--EXPECTF-- +Testing 1 +bool(false) +Testing 2 +bool(false) +Testing 3 +bool(false) +Testing 4 +bool(false) +Testing 5 +bool(false) +Testing 6 +bool(false) +Testing 7 +bool(false) +Testing 8 +bool(false) +Testing 9 +bool(false) +Testing 10 +bool(false) +Testing 11 +bool(false) +Testing 12 +bool(false) +Testing 13 +bool(false) +Testing 14 +bool(false) +Testing 15 +bool(false) +Testing 16 +bool(false) +Testing 17 +bool(false) +Testing 18 +bool(false) +Testing 19 +bool(false) +Testing 20 +bool(false) +Testing 21 +bool(false) +Testing 22 +bool(false) +Testing 23 +bool(false) +Testing 24 +bool(false) +Testing 25 +bool(false) +Testing 26 +bool(false) +Testing 27 +bool(false) +Testing 28 +bool(false) +Testing 29 +bool(false) +Testing 30 +bool(false) +Testing 31 +bool(false) +Testing 32 +resource(%d) of type (process) +%s.bat +"¬epad.exe +Testing 33 +bool(false) +Testing 34 +bool(false) +Testing 35 +bool(false) +Testing 36 +bool(false) +Testing 37 +bool(false) +Testing 38 +bool(false) +Testing 39 +bool(false) +Testing 40 +bool(false) +Testing 41 +bool(false) +Testing 42 +bool(false) +Testing 43 +bool(false) +Testing 44 +bool(false) +Testing 45 +bool(false) +Testing 46 +resource(%d) of type (process) +'"%s.bat."' is not recognized as an internal or external command, +operable program or batch file. +Testing 47 +bool(false) +Testing 48 +bool(false) +Testing 49 +bool(false) +Testing 50 +bool(false) +Testing 51 +bool(false) +Testing 52 +bool(false) +Testing 53 +bool(false) +Testing 54 +bool(false) +Testing 55 +bool(false) +Testing 56 +bool(false) +Testing 57 +bool(false) +Testing 58 +bool(false) +Testing 59 +bool(false) +Testing 60 +bool(false) +Testing 61 +bool(false) +Testing 62 +bool(false) +Testing 63 +bool(false) +Testing 64 +bool(false) +Testing 65 +bool(false) +Testing 66 +bool(false) +Testing 67 +bool(false) +Testing 68 +bool(false) +Testing 69 +bool(false) +Testing 70 +bool(false) +Testing 71 +bool(false) +Testing 72 +bool(false) +Testing 73 +bool(false) +Testing 74 +bool(false) +Testing 75 +bool(false) +Testing 76 +bool(false) +Testing 77 +bool(false) +Testing 78 +bool(false) +Testing 79 +bool(false) +Testing 80 +bool(false) +Testing 81 +bool(false) +Testing 82 +bool(false) +Testing 83 +bool(false) +Testing 84 +bool(false) +Testing 85 +bool(false) +Testing 86 +bool(false) +Testing 87 +bool(false) +Testing 88 +bool(false) +Testing 89 +bool(false) +Testing 90 +bool(false) +Testing 91 +bool(false) +Testing 92 +bool(false) +Testing 93 +bool(false) +Testing 94 +bool(false) +Testing 95 +bool(false) +Testing 96 +bool(false) +Testing 97 +bool(false) +Testing 98 +bool(false) +Testing 99 +bool(false) +Testing 100 +bool(false) +Testing 101 +bool(false) +Testing 102 +bool(false) +Testing 103 +bool(false) +Testing 104 +bool(false) +Testing 105 +bool(false) +Testing 106 +bool(false) +Testing 107 +bool(false) +Testing 108 +bool(false) +Testing 109 +bool(false) +Testing 110 +bool(false) +Testing 111 +bool(false) +Testing 112 +bool(false) +Testing 113 +bool(false) +Testing 114 +bool(false) +Testing 115 +bool(false) +Testing 116 +bool(false) +Testing 117 +bool(false) +Testing 118 +bool(false) +Testing 119 +bool(false) +Testing 120 +bool(false) +Testing 121 +bool(false) +Testing 122 +bool(false) +Testing 123 +bool(false) +Testing 124 +bool(false) +Testing 125 +bool(false) +Testing 126 +bool(false) +Testing 127 +bool(false) +Testing 128 +bool(false) +Testing 129 +bool(false) +Testing 130 +bool(false) +Testing 131 +bool(false) +Testing 132 +bool(false) +Testing 133 +bool(false) +Testing 134 +bool(false) +Testing 135 +bool(false) +Testing 136 +bool(false) +Testing 137 +bool(false) +Testing 138 +bool(false) +Testing 139 +bool(false) +Testing 140 +bool(false) +Testing 141 +bool(false) +Testing 142 +bool(false) +Testing 143 +bool(false) +Testing 144 +bool(false) +Testing 145 +bool(false) +Testing 146 +bool(false) +Testing 147 +bool(false) +Testing 148 +bool(false) +Testing 149 +bool(false) +Testing 150 +bool(false) +Testing 151 +bool(false) +Testing 152 +bool(false) +Testing 153 +bool(false) +Testing 154 +bool(false) +Testing 155 +bool(false) +Testing 156 +bool(false) +Testing 157 +bool(false) +Testing 158 +bool(false) +Testing 159 +bool(false) +Testing 160 +bool(false) +Testing 161 +bool(false) +Testing 162 +bool(false) +Testing 163 +bool(false) +Testing 164 +bool(false) +Testing 165 +bool(false) +Testing 166 +bool(false) +Testing 167 +bool(false) +Testing 168 +bool(false) +Testing 169 +bool(false) +Testing 170 +bool(false) +Testing 171 +bool(false) +Testing 172 +bool(false) +Testing 173 +bool(false) +Testing 174 +bool(false) +Testing 175 +bool(false) +Testing 176 +bool(false) +Testing 177 +bool(false) +Testing 178 +bool(false) +Testing 179 +bool(false) +Testing 180 +bool(false) +Testing 181 +bool(false) +Testing 182 +bool(false) +Testing 183 +bool(false) +Testing 184 +bool(false) +Testing 185 +bool(false) +Testing 186 +bool(false) +Testing 187 +bool(false) +Testing 188 +bool(false) +Testing 189 +bool(false) +Testing 190 +bool(false) +Testing 191 +bool(false) +Testing 192 +bool(false) +Testing 193 +bool(false) +Testing 194 +bool(false) +Testing 195 +bool(false) +Testing 196 +bool(false) +Testing 197 +bool(false) +Testing 198 +bool(false) +Testing 199 +bool(false) +Testing 200 +bool(false) +Testing 201 +bool(false) +Testing 202 +bool(false) +Testing 203 +bool(false) +Testing 204 +bool(false) +Testing 205 +bool(false) +Testing 206 +bool(false) +Testing 207 +bool(false) +Testing 208 +bool(false) +Testing 209 +bool(false) +Testing 210 +bool(false) +Testing 211 +bool(false) +Testing 212 +bool(false) +Testing 213 +bool(false) +Testing 214 +bool(false) +Testing 215 +bool(false) +Testing 216 +bool(false) +Testing 217 +bool(false) +Testing 218 +bool(false) +Testing 219 +bool(false) +Testing 220 +bool(false) +Testing 221 +bool(false) +Testing 222 +bool(false) +Testing 223 +bool(false) +Testing 224 +bool(false) +Testing 225 +bool(false) +Testing 226 +bool(false) +Testing 227 +bool(false) +Testing 228 +bool(false) +Testing 229 +bool(false) +Testing 230 +bool(false) +Testing 231 +bool(false) +Testing 232 +bool(false) +Testing 233 +bool(false) +Testing 234 +bool(false) +Testing 235 +bool(false) +Testing 236 +bool(false) +Testing 237 +bool(false) +Testing 238 +bool(false) +Testing 239 +bool(false) +Testing 240 +bool(false) +Testing 241 +bool(false) +Testing 242 +bool(false) +Testing 243 +bool(false) +Testing 244 +bool(false) +Testing 245 +bool(false) +Testing 246 +bool(false) +Testing 247 +bool(false) +Testing 248 +bool(false) +Testing 249 +bool(false) +Testing 250 +bool(false) +Testing 251 +bool(false) +Testing 252 +bool(false) +Testing 253 +bool(false) +Testing 254 +bool(false) +Testing 255 +bool(false) +--CLEAN-- + diff --git a/main/php_version.h b/main/php_version.h index a7b35a63a9285..876ccc81aa168 100644 --- a/main/php_version.h +++ b/main/php_version.h @@ -2,7 +2,7 @@ /* edit configure.ac to change version number */ #define PHP_MAJOR_VERSION 8 #define PHP_MINOR_VERSION 1 -#define PHP_RELEASE_VERSION 29 -#define PHP_EXTRA_VERSION "-dev" -#define PHP_VERSION "8.1.29-dev" -#define PHP_VERSION_ID 80129 +#define PHP_RELEASE_VERSION 30 +#define PHP_EXTRA_VERSION "" +#define PHP_VERSION "8.1.30" +#define PHP_VERSION_ID 80130 diff --git a/main/rfc1867.c b/main/rfc1867.c index 2ddad7950fdf0..83d141d38b112 100644 --- a/main/rfc1867.c +++ b/main/rfc1867.c @@ -751,6 +751,13 @@ SAPI_API SAPI_POST_HANDLER_FUNC(rfc1867_post_handler) /* {{{ */ boundary_len = boundary_end-boundary; } + /* Boundaries larger than FILLUNIT-strlen("\r\n--") characters lead to + * erroneous parsing */ + if (boundary_len > FILLUNIT-strlen("\r\n--")) { + sapi_module.sapi_error(E_WARNING, "Boundary too large in multipart/form-data POST data"); + return; + } + /* Initialize the buffer */ if (!(mbuff = multipart_buffer_new(boundary, boundary_len))) { sapi_module.sapi_error(E_WARNING, "Unable to initialize the input buffer"); diff --git a/sapi/cgi/cgi_main.c b/sapi/cgi/cgi_main.c index 499a7932bed17..c7bb7ccabf95d 100644 --- a/sapi/cgi/cgi_main.c +++ b/sapi/cgi/cgi_main.c @@ -1748,7 +1748,6 @@ int main(int argc, char *argv[]) int status = 0; #endif char *query_string; - char *decoded_query_string; int skip_getopt = 0; #if defined(SIGPIPE) && defined(SIG_IGN) @@ -1798,10 +1797,20 @@ int main(int argc, char *argv[]) } } + /* Apache CGI will pass the query string to the command line if it doesn't contain a '='. + * This can create an issue where a malicious request can pass command line arguments to + * the executable. Ideally we skip argument parsing when we're in cgi or fastcgi mode, + * but that breaks PHP scripts on Linux with a hashbang: `#!/php-cgi -d option=value`. + * Therefore, this code only prevents passing arguments if the query string starts with a '-'. + * Similarly, scripts spawned in subprocesses on Windows may have the same issue. + * However, Windows has lots of conversion rules and command line parsing rules that + * are too difficult and dangerous to reliably emulate. */ if((query_string = getenv("QUERY_STRING")) != NULL && strchr(query_string, '=') == NULL) { - /* we've got query string that has no = - apache CGI will pass it to command line */ +#ifdef PHP_WIN32 + skip_getopt = cgi || fastcgi; +#else unsigned char *p; - decoded_query_string = strdup(query_string); + char *decoded_query_string = strdup(query_string); php_url_decode(decoded_query_string, strlen(decoded_query_string)); for (p = (unsigned char *)decoded_query_string; *p && *p <= ' '; p++) { /* skip all leading spaces */ @@ -1809,7 +1818,9 @@ int main(int argc, char *argv[]) if(*p == '-') { skip_getopt = 1; } + free(decoded_query_string); +#endif } while (!skip_getopt && (c = php_getopt(argc, argv, OPTIONS, &php_optarg, &php_optind, 0, 2)) != -1) { @@ -1899,18 +1910,17 @@ int main(int argc, char *argv[]) /* check force_cgi after startup, so we have proper output */ if (cgi && CGIG(force_redirect)) { - /* Apache will generate REDIRECT_STATUS, - * Netscape and redirect.so will generate HTTP_REDIRECT_STATUS. - * redirect.so and installation instructions available from - * http://www.koehntopp.de/php. - * -- kk@netuse.de - */ - if (!getenv("REDIRECT_STATUS") && - !getenv ("HTTP_REDIRECT_STATUS") && - /* this is to allow a different env var to be configured - * in case some server does something different than above */ - (!CGIG(redirect_status_env) || !getenv(CGIG(redirect_status_env))) - ) { + /* This is to allow a different environment variable to be configured + * in case the we cannot auto-detect which environment variable to use. + * Checking this first to allow user overrides in case the environment + * variable can be set by an untrusted party. */ + const char *redirect_status_env = CGIG(redirect_status_env); + if (!redirect_status_env) { + /* Apache will generate REDIRECT_STATUS. */ + redirect_status_env = "REDIRECT_STATUS"; + } + + if (!getenv(redirect_status_env)) { zend_try { SG(sapi_headers).http_response_code = 400; PUTS("Security Alert! The PHP CGI cannot be accessed directly.\n\n\ diff --git a/sapi/cgi/tests/ghsa-3qgc-jrrr-25jv.phpt b/sapi/cgi/tests/ghsa-3qgc-jrrr-25jv.phpt new file mode 100644 index 0000000000000..fd2fcdfbf897d --- /dev/null +++ b/sapi/cgi/tests/ghsa-3qgc-jrrr-25jv.phpt @@ -0,0 +1,38 @@ +--TEST-- +GHSA-3qgc-jrrr-25jv +--SKIPIF-- + +--FILE-- +'; +file_put_contents($filename, $script); + +$php = get_cgi_path(); +reset_env_vars(); + +putenv("SERVER_NAME=Test"); +putenv("SCRIPT_FILENAME=$filename"); +putenv("QUERY_STRING=%ads"); +putenv("REDIRECT_STATUS=1"); + +passthru("$php -s"); + +?> +--CLEAN-- + +--EXPECTF-- +X-Powered-By: PHP/%s +Content-type: %s + +hello world diff --git a/sapi/fpm/fpm/fpm_stdio.c b/sapi/fpm/fpm/fpm_stdio.c index 8f71e8cbfcd08..dec540d17aca8 100644 --- a/sapi/fpm/fpm/fpm_stdio.c +++ b/sapi/fpm/fpm/fpm_stdio.c @@ -229,7 +229,7 @@ static void fpm_stdio_child_said(struct fpm_event_s *ev, short which, void *arg) if ((sizeof(FPM_STDIO_CMD_FLUSH) - cmd_pos) <= in_buf && !memcmp(buf, &FPM_STDIO_CMD_FLUSH[cmd_pos], sizeof(FPM_STDIO_CMD_FLUSH) - cmd_pos)) { zlog_stream_finish(log_stream); - start = cmd_pos; + start = sizeof(FPM_STDIO_CMD_FLUSH) - cmd_pos; } else { zlog_stream_str(log_stream, &FPM_STDIO_CMD_FLUSH[0], cmd_pos); } diff --git a/sapi/fpm/tests/log-bwp-msg-flush-split-sep-pos-end.phpt b/sapi/fpm/tests/log-bwp-msg-flush-split-sep-pos-end.phpt new file mode 100644 index 0000000000000..528263200803e --- /dev/null +++ b/sapi/fpm/tests/log-bwp-msg-flush-split-sep-pos-end.phpt @@ -0,0 +1,47 @@ +--TEST-- +FPM: Buffered worker output plain log with msg with flush split position towards separator end +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->request()->expectEmptyBody(); +$tester->expectLogLine(str_repeat('a', 1013) . "Quarkslab", decorated: false); +$tester->expectLogLine("Quarkslab", decorated: false); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/sapi/fpm/tests/log-bwp-msg-flush-split-sep-pos-start.phpt b/sapi/fpm/tests/log-bwp-msg-flush-split-sep-pos-start.phpt new file mode 100644 index 0000000000000..3490593855328 --- /dev/null +++ b/sapi/fpm/tests/log-bwp-msg-flush-split-sep-pos-start.phpt @@ -0,0 +1,47 @@ +--TEST-- +FPM: Buffered worker output plain log with msg with flush split position towards separator start +--SKIPIF-- + +--FILE-- +start(); +$tester->expectLogStartNotices(); +$tester->request()->expectEmptyBody(); +$tester->expectLogLine(str_repeat('a', 1009) . "Quarkslab", decorated: false); +$tester->expectLogLine("Quarkslab", decorated: false); +$tester->terminate(); +$tester->expectLogTerminatingNotices(); +$tester->close(); + +?> +Done +--EXPECT-- +Done +--CLEAN-- + diff --git a/tests/basic/GHSA-9pqp-7h25-4f32.inc b/tests/basic/GHSA-9pqp-7h25-4f32.inc new file mode 100644 index 0000000000000..adf72a361a2cb --- /dev/null +++ b/tests/basic/GHSA-9pqp-7h25-4f32.inc @@ -0,0 +1,3 @@ + +--FILE-- + '1', + 'CONTENT_TYPE' => "multipart/form-data; boundary=$boundary", + 'CONTENT_LENGTH' => strlen($body), + 'REQUEST_METHOD' => 'POST', + 'SCRIPT_FILENAME' => __DIR__ . '/GHSA-9pqp-7h25-4f32.inc', + ]); + + $spec = [ + 0 => ['pipe', 'r'], + 1 => STDOUT, + 2 => STDOUT, + ]; + + $pipes = []; + + print "Starting...\n"; + + $handle = proc_open($cmd, $spec, $pipes, getcwd(), $env); + + fwrite($pipes[0], $body); + + $status = proc_close($handle); + + print "\n"; +} + +for ($offset = -1; $offset <= 1; $offset++) { + test(FILLUNIT - strlen("\r\n--") + $offset); +} + +?> +--EXPECTF-- +Boundary len: 5115 +Starting... +X-Powered-By: %s +Content-type: text/html; charset=UTF-8 + +Hello world +array(1) { + ["koko"]=> + string(5124) "BBB +--AAA%sCCC" +} + +Boundary len: 5116 +Starting... +X-Powered-By: %s +Content-type: text/html; charset=UTF-8 + +Hello world +array(1) { + ["koko"]=> + string(5125) "BBB +--AAA%sCCC" +} + +Boundary len: 5117 +Starting... +X-Powered-By: %s +Content-type: text/html; charset=UTF-8 + +
+Warning: Boundary too large in multipart/form-data POST data in Unknown on line 0
+Hello world +array(0) { +} +