From 4b15f5d4ec750b31ec8911f5eb0915a45f96feca Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sat, 20 Apr 2024 17:09:35 +0200 Subject: [PATCH 01/13] Fix GHSA-9fcc-425m-g385: bypass CVE-2024-1874 The old code checked for suffixes but didn't take into account trailing whitespace. Furthermore, there is peculiar behaviour with trailing dots too. This all happens because of the special path-handling code inside CreateProcessW. By studying Wine's code, we can see that CreateProcessInternalW calls get_file_name [1] in our case because we haven't provided an application name. That code gets the first whitespace-delimited string into app_name excluding the quotes. It's then passed to create_process_params [2] where there is the path handling code that transforms the command line argument to an image path [3]. Inside Wine, the extension check if performed after these transformations [4]. By doing the same thing in PHP we match the behaviour and can properly match the extension even in the given edge cases. [1] https://github.com/wine-mirror/wine/blob/166895ae3ad3890ad946a309d0fd85e89ea3630e/dlls/kernelbase/process.c#L542-L543 [2] https://github.com/wine-mirror/wine/blob/166895ae3ad3890ad946a309d0fd85e89ea3630e/dlls/kernelbase/process.c#L565 [3] https://github.com/wine-mirror/wine/blob/166895ae3ad3890ad946a309d0fd85e89ea3630e/dlls/kernelbase/process.c#L150-L151 [4] https://github.com/wine-mirror/wine/blob/166895ae3ad3890ad946a309d0fd85e89ea3630e/dlls/kernelbase/process.c#L647-L654 --- ext/standard/proc_open.c | 59 +- .../ghsa-9fcc-425m-g385_001.phpt | 56 ++ .../ghsa-9fcc-425m-g385_002.phpt | 66 +++ .../ghsa-9fcc-425m-g385_003.phpt | 550 ++++++++++++++++++ 4 files changed, 697 insertions(+), 34 deletions(-) create mode 100644 ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_001.phpt create mode 100644 ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_002.phpt create mode 100644 ext/standard/tests/general_functions/ghsa-9fcc-425m-g385_003.phpt 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-- + From 938267314835de3c2ed1a3da4f2959f1d2709468 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Fri, 17 May 2024 21:51:30 +0200 Subject: [PATCH 02/13] Fix GHSA-3qgc-jrrr-25jv The original code is error-prone due to the "best fit mapping" that happens with the argument parsing but not with the query string. When we get a non-ASCII character, try to remap it and see if it becomes a hyphen. An alternative approach is to create a custom main `wmain` receiving wide-character variations that does the ANSI transformation with the best-fit mapping, but that's more error-prone and could cause unexpected breakage. Another alternative was just don't doing this check altogether and always check for `cgi || fastcgi` instead, but that breaks real-world use-cases. --- sapi/cgi/cgi_main.c | 23 ++++++++++++++- sapi/cgi/tests/ghsa-3qgc-jrrr-25jv.phpt | 38 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 sapi/cgi/tests/ghsa-3qgc-jrrr-25jv.phpt diff --git a/sapi/cgi/cgi_main.c b/sapi/cgi/cgi_main.c index 499a7932bed17..2c1fa9332dfbb 100644 --- a/sapi/cgi/cgi_main.c +++ b/sapi/cgi/cgi_main.c @@ -1798,8 +1798,13 @@ 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. */ 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 */ unsigned char *p; decoded_query_string = strdup(query_string); php_url_decode(decoded_query_string, strlen(decoded_query_string)); @@ -1809,6 +1814,22 @@ int main(int argc, char *argv[]) if(*p == '-') { skip_getopt = 1; } + + /* On Windows we have to take into account the "best fit" mapping behaviour. */ +#ifdef PHP_WIN32 + if (*p >= 0x80) { + wchar_t wide_buf[1]; + wide_buf[0] = *p; + char char_buf[4]; + size_t wide_buf_len = sizeof(wide_buf) / sizeof(wide_buf[0]); + size_t char_buf_len = sizeof(char_buf) / sizeof(char_buf[0]); + if (WideCharToMultiByte(CP_ACP, 0, wide_buf, wide_buf_len, char_buf, char_buf_len, NULL, NULL) == 0 + || char_buf[0] == '-') { + skip_getopt = 1; + } + } +#endif + free(decoded_query_string); } 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 From 7e0e3cc820c493301409a0ce2b6ef95e0ab06b0c Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Wed, 22 May 2024 22:25:02 +0200 Subject: [PATCH 03/13] Fix GHSA-w8qr-v226-r27w We should not early-out with success status if we found an ipv6 hostname, we should keep checking the rest of the conditions. Because integrating the if-check of the ipv6 hostname in the "Validate domain" if-check made the code hard to read, I extracted the condition out to a separate function. This also required to make a few pointers const in order to have some clean code. --- ext/filter/logical_filters.c | 35 ++++++++++--------- ext/filter/tests/ghsa-w8qr-v226-r27w.phpt | 41 +++++++++++++++++++++++ 2 files changed, 61 insertions(+), 15 deletions(-) create mode 100644 ext/filter/tests/ghsa-w8qr-v226-r27w.phpt 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]/" From 557e09f67802bf9033c7c3477b4dc1d6147cbd29 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Wed, 5 Jun 2024 00:39:47 -0500 Subject: [PATCH 04/13] Update NEWS Co-authored-by: Eric Mann --- NEWS | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/NEWS b/NEWS index f9b8fb276472d..3cfa614f0cd9b 100644 --- a/NEWS +++ b/NEWS @@ -1,8 +1,28 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| -?? ??? ????, PHP 8.1.29 +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 +51,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) From a87ccc7ca2644e327c210e68b4d98df98ad33523 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Wed, 5 Jun 2024 00:48:17 -0500 Subject: [PATCH 05/13] PHP-8.1 is now for PHP 8.1.30-dev --- NEWS | 4 ++++ Zend/zend.h | 2 +- configure.ac | 2 +- main/php_version.h | 6 +++--- 4 files changed, 9 insertions(+), 5 deletions(-) diff --git a/NEWS b/NEWS index 3cfa614f0cd9b..3bc2c282ec0a3 100644 --- a/NEWS +++ b/NEWS @@ -1,5 +1,9 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| +?? ??? ????, PHP 8.1.30 + + + 06 Jun 2024, PHP 8.1.29 - CGI: diff --git a/Zend/zend.h b/Zend/zend.h index 9a414960e2a87..56ce93f685da9 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-dev" #define ZEND_ENGINE_3 diff --git a/configure.ac b/configure.ac index 83d50445be205..2c21f30ff2611 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-dev],[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/main/php_version.h b/main/php_version.h index a7b35a63a9285..3327cdc332e04 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_RELEASE_VERSION 30 #define PHP_EXTRA_VERSION "-dev" -#define PHP_VERSION "8.1.29-dev" -#define PHP_VERSION_ID 80129 +#define PHP_VERSION "8.1.30-dev" +#define PHP_VERSION_ID 80130 From d65a1e6f9171e01ca81adc8d9dd1daf3125efc36 Mon Sep 17 00:00:00 2001 From: Arnaud Le Blanc Date: Mon, 9 Sep 2024 15:22:07 +0200 Subject: [PATCH 06/13] Fix GHSA-9pqp-7h25-4f32 multipart/form-data boundaries larger than the read buffer result in erroneous parsing, which violates data integrity. Limit boundary size, as allowed by RFC 1521: Encapsulation boundaries [...] must be no longer than 70 characters, not counting the two leading hyphens. We correctly parse payloads with boundaries of length up to FILLUNIT-strlen("\r\n--") bytes, so allow this for BC. --- main/rfc1867.c | 7 ++ tests/basic/GHSA-9pqp-7h25-4f32.inc | 3 + tests/basic/GHSA-9pqp-7h25-4f32.phpt | 100 +++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 tests/basic/GHSA-9pqp-7h25-4f32.inc create mode 100644 tests/basic/GHSA-9pqp-7h25-4f32.phpt 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/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) { +} + From 4b9cd27ff5c0177dcb160caeae1ea79e761ada58 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Fri, 14 Jun 2024 19:49:22 +0200 Subject: [PATCH 07/13] Fix GHSA-p99j-rfp4-xqvq It's no use trying to work around whatever the operating system and Apache do because we'll be fighting that until eternity. Change the skip_getopt condition such that when we're running in CGI or FastCGI mode we always skip the argument parsing. This is a BC break, but this seems to be the only way to get rid of this class of issues. --- sapi/cgi/cgi_main.c | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/sapi/cgi/cgi_main.c b/sapi/cgi/cgi_main.c index 2c1fa9332dfbb..fba0563aa180e 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) @@ -1803,10 +1802,15 @@ int main(int argc, char *argv[]) * 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. */ + * 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) { +#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 */ @@ -1815,22 +1819,8 @@ int main(int argc, char *argv[]) skip_getopt = 1; } - /* On Windows we have to take into account the "best fit" mapping behaviour. */ -#ifdef PHP_WIN32 - if (*p >= 0x80) { - wchar_t wide_buf[1]; - wide_buf[0] = *p; - char char_buf[4]; - size_t wide_buf_len = sizeof(wide_buf) / sizeof(wide_buf[0]); - size_t char_buf_len = sizeof(char_buf) / sizeof(char_buf[0]); - if (WideCharToMultiByte(CP_ACP, 0, wide_buf, wide_buf_len, char_buf, char_buf_len, NULL, NULL) == 0 - || char_buf[0] == '-') { - skip_getopt = 1; - } - } -#endif - free(decoded_query_string); +#endif } while (!skip_getopt && (c = php_getopt(argc, argv, OPTIONS, &php_optarg, &php_optind, 0, 2)) != -1) { From c1c14c8a0f1067baac1b22474df19266afe21a4d Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Tue, 18 Jun 2024 21:28:26 +0200 Subject: [PATCH 08/13] Fix GHSA-94p6-54jq-9mwp Apache only generates REDIRECT_STATUS, so explicitly check for that if the server name is Apache, don't allow other variable names. Furthermore, redirect.so and Netscape no longer exist, so remove those entries as we can't check their server name anymore. We now also check for the configuration override *first* such that it always take precedence. This would allow for a mitigation path if something like this happens in the future. --- sapi/cgi/cgi_main.c | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/sapi/cgi/cgi_main.c b/sapi/cgi/cgi_main.c index fba0563aa180e..c7bb7ccabf95d 100644 --- a/sapi/cgi/cgi_main.c +++ b/sapi/cgi/cgi_main.c @@ -1910,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\ From 4580b8b3e1495df45a894ab928b23067159eedb8 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 12 Sep 2024 13:11:11 +0100 Subject: [PATCH 09/13] Fix GHSA-865w-9rf3-2wh5: FPM: Logs from childrens may be altered --- sapi/fpm/fpm/fpm_stdio.c | 2 +- .../log-bwp-msg-flush-split-sep-pos-end.phpt | 47 +++++++++++++++++++ ...log-bwp-msg-flush-split-sep-pos-start.phpt | 47 +++++++++++++++++++ 3 files changed, 95 insertions(+), 1 deletion(-) create mode 100644 sapi/fpm/tests/log-bwp-msg-flush-split-sep-pos-end.phpt create mode 100644 sapi/fpm/tests/log-bwp-msg-flush-split-sep-pos-start.phpt 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-- + From 8d87bc3e266d4848f74dd4b2b7ee5fe4d666bab7 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 23 Sep 2024 12:09:57 +0100 Subject: [PATCH 10/13] Update NEWS with security fixes info --- NEWS | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 3bc2c282ec0a3..1cd4bdd779df6 100644 --- a/NEWS +++ b/NEWS @@ -1,8 +1,21 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| -?? ??? ????, PHP 8.1.30 +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 + byppassible 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 From 4bcc7d5778e70858b4c54c07d21f40ffb09979e2 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Mon, 23 Sep 2024 18:54:31 +0100 Subject: [PATCH 11/13] Skip GHSA-9pqp-7h25-4f32 test on Windows --- tests/basic/GHSA-9pqp-7h25-4f32.phpt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/basic/GHSA-9pqp-7h25-4f32.phpt b/tests/basic/GHSA-9pqp-7h25-4f32.phpt index af81916370500..29bcb6557d5a2 100644 --- a/tests/basic/GHSA-9pqp-7h25-4f32.phpt +++ b/tests/basic/GHSA-9pqp-7h25-4f32.phpt @@ -5,6 +5,9 @@ GHSA-9pqp-7h25-4f32 if (!getenv('TEST_PHP_CGI_EXECUTABLE')) { die("skip php-cgi not available"); } +if (substr(PHP_OS, 0, 3) == 'WIN') { + die("skip not for Windows in CI - probably resource issue"); +} ?> --FILE-- Date: Mon, 23 Sep 2024 20:50:51 +0100 Subject: [PATCH 12/13] [skip ci] Fix typo in NEWS Co-authored-by: Niels Dossche <7771979+nielsdos@users.noreply.github.com> --- NEWS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS b/NEWS index 1cd4bdd779df6..e3e7a5613be3a 100644 --- a/NEWS +++ b/NEWS @@ -6,7 +6,7 @@ PHP NEWS . 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 - byppassible due to the environment variable collision). (CVE-2024-8927) + bypassable due to the environment variable collision). (CVE-2024-8927) (nielsdos) - FPM: From 773c0eda4ac8181c59a6510a39c37710679277a4 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Thu, 26 Sep 2024 11:57:18 -0500 Subject: [PATCH 13/13] Update versions for PHP 8.1.30 --- Zend/zend.h | 2 +- configure.ac | 2 +- main/php_version.h | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Zend/zend.h b/Zend/zend.h index 56ce93f685da9..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.30-dev" +#define ZEND_VERSION "4.1.30" #define ZEND_ENGINE_3 diff --git a/configure.ac b/configure.ac index 2c21f30ff2611..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.30-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/main/php_version.h b/main/php_version.h index 3327cdc332e04..876ccc81aa168 100644 --- a/main/php_version.h +++ b/main/php_version.h @@ -3,6 +3,6 @@ #define PHP_MAJOR_VERSION 8 #define PHP_MINOR_VERSION 1 #define PHP_RELEASE_VERSION 30 -#define PHP_EXTRA_VERSION "-dev" -#define PHP_VERSION "8.1.30-dev" +#define PHP_EXTRA_VERSION "" +#define PHP_VERSION "8.1.30" #define PHP_VERSION_ID 80130