From e3c784f2bfb6029b49d27783b2efc87ee6923f79 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Thu, 15 Feb 2024 23:53:49 +0000 Subject: [PATCH 1/7] Add proc_open escaping for cmd file execution --- ext/standard/proc_open.c | 86 +++++++++++++++++-- .../ghsa-pc52-254m-w9w7_1.phpt | 29 +++++++ .../ghsa-pc52-254m-w9w7_2.phpt | 29 +++++++ .../ghsa-pc52-254m-w9w7_3.phpt | 29 +++++++ 4 files changed, 168 insertions(+), 5 deletions(-) create mode 100644 ext/standard/tests/general_functions/ghsa-pc52-254m-w9w7_1.phpt create mode 100644 ext/standard/tests/general_functions/ghsa-pc52-254m-w9w7_2.phpt create mode 100644 ext/standard/tests/general_functions/ghsa-pc52-254m-w9w7_3.phpt diff --git a/ext/standard/proc_open.c b/ext/standard/proc_open.c index 3f8eaafd6d2..8aae5407265 100644 --- a/ext/standard/proc_open.c +++ b/ext/standard/proc_open.c @@ -492,11 +492,32 @@ static void append_backslashes(smart_str *str, size_t num_bs) } } -/* See https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments */ -static void append_win_escaped_arg(smart_str *str, zend_string *arg) +const char *special_chars = "()!^\"<>&|%"; + +static bool is_special_character_present(const zend_string *arg) +{ + for (size_t i = 0; i < ZSTR_LEN(arg); ++i) { + if (strchr(special_chars, ZSTR_VAL(arg)[i]) != NULL) { + return true; + } + } + return false; +} + +/* See https://docs.microsoft.com/en-us/cpp/cpp/parsing-cpp-command-line-arguments and + * https://learn.microsoft.com/en-us/archive/blogs/twistylittlepassagesallalike/everyone-quotes-command-line-arguments-the-wrong-way */ +static void append_win_escaped_arg(smart_str *str, zend_string *arg, bool is_cmd_argument) { size_t num_bs = 0; + bool has_special_character = false; + if (is_cmd_argument) { + has_special_character = is_special_character_present(arg); + if (has_special_character) { + /* Escape double quote with ^ if executed by cmd.exe. */ + smart_str_appendc(str, '^'); + } + } smart_str_appendc(str, '"'); for (size_t i = 0; i < ZSTR_LEN(arg); ++i) { char c = ZSTR_VAL(arg)[i]; @@ -510,18 +531,71 @@ static void append_win_escaped_arg(smart_str *str, zend_string *arg) num_bs = num_bs * 2 + 1; } append_backslashes(str, num_bs); + if (has_special_character && strchr(special_chars, c) != NULL) { + /* Escape special chars with ^ if executed by cmd.exe. */ + smart_str_appendc(str, '^'); + } smart_str_appendc(str, c); num_bs = 0; } append_backslashes(str, num_bs * 2); + if (has_special_character) { + /* Escape double quote with ^ if executed by cmd.exe. */ + smart_str_appendc(str, '^'); + } 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); + + if (suffix_len > str_len) { + return -1; /* Suffix is longer than string, cannot match. */ + } + + /* Compare the end of the string with the suffix, ignoring case. */ + return _stricmp(str + (str_len - suffix_len), suffix); +} + +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; + } + + /* 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; + } + + /* Find the last dot in the filename after the last directory separator. */ + char *extension = NULL; + if (last_separator != NULL) { + extension = strrchr(last_separator, '.'); + } else { + extension = strrchr(prog_name, '.'); + } + + if (extension == NULL || extension == prog_name) { + /* No file extension found, it is not batch file. */ + return false; + } + + /* 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; +} + static zend_string *create_win_command_from_args(HashTable *args) { smart_str str = {0}; zval *arg_zv; - bool is_prog_name = 1; + bool is_prog_name = true; + bool is_cmd_execution = false; int elem_num = 0; ZEND_HASH_FOREACH_VAL(args, arg_zv) { @@ -531,11 +605,13 @@ static zend_string *create_win_command_from_args(HashTable *args) return NULL; } - if (!is_prog_name) { + if (is_prog_name) { + is_cmd_execution = is_executed_by_cmd(ZSTR_VAL(arg_str)); + } else { smart_str_appendc(&str, ' '); } - append_win_escaped_arg(&str, arg_str); + append_win_escaped_arg(&str, arg_str, !is_prog_name && is_cmd_execution); is_prog_name = 0; zend_string_release(arg_str); diff --git a/ext/standard/tests/general_functions/ghsa-pc52-254m-w9w7_1.phpt b/ext/standard/tests/general_functions/ghsa-pc52-254m-w9w7_1.phpt new file mode 100644 index 00000000000..8d0939cdf1b --- /dev/null +++ b/ext/standard/tests/general_functions/ghsa-pc52-254m-w9w7_1.phpt @@ -0,0 +1,29 @@ +--TEST-- +GHSA-54hq-v5wp-fqgv - proc_open does not correctly escape args for bat files +--SKIPIF-- + +--FILE-- + +--EXPECT-- +"¬epad.exe +--CLEAN-- + diff --git a/ext/standard/tests/general_functions/ghsa-pc52-254m-w9w7_2.phpt b/ext/standard/tests/general_functions/ghsa-pc52-254m-w9w7_2.phpt new file mode 100644 index 00000000000..a1e39d7ef9b --- /dev/null +++ b/ext/standard/tests/general_functions/ghsa-pc52-254m-w9w7_2.phpt @@ -0,0 +1,29 @@ +--TEST-- +GHSA-54hq-v5wp-fqgv - proc_open does not correctly escape args for cmd files +--SKIPIF-- + +--FILE-- +^()!.exe"], $descriptorspec, $pipes); +proc_close($proc); + +?> +--EXPECT-- +"¬epad<>^()!.exe +--CLEAN-- + diff --git a/ext/standard/tests/general_functions/ghsa-pc52-254m-w9w7_3.phpt b/ext/standard/tests/general_functions/ghsa-pc52-254m-w9w7_3.phpt new file mode 100644 index 00000000000..69f12d7b358 --- /dev/null +++ b/ext/standard/tests/general_functions/ghsa-pc52-254m-w9w7_3.phpt @@ -0,0 +1,29 @@ +--TEST-- +GHSA-54hq-v5wp-fqgv - proc_open does not correctly escape args for cmd executing batch files +--SKIPIF-- + +--FILE-- + +--EXPECT-- +"¬epad.exe +--CLEAN-- + From 093c08af25fb323efa0c8e6154aa9fdeae3d3b53 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sun, 17 Mar 2024 21:04:47 +0100 Subject: [PATCH 2/7] Fix GHSA-wpj3-hf5j-x4v4: __Host-/__Secure- cookie bypass due to partial CVE-2022-31629 fix The check happened too early as later code paths may perform more mangling rules. Move the check downwards right before adding the actual variable. --- ext/standard/tests/ghsa-wpj3-hf5j-x4v4.phpt | 63 +++++++++++++++++++++ main/php_variables.c | 41 +++++++++----- 2 files changed, 90 insertions(+), 14 deletions(-) create mode 100644 ext/standard/tests/ghsa-wpj3-hf5j-x4v4.phpt diff --git a/ext/standard/tests/ghsa-wpj3-hf5j-x4v4.phpt b/ext/standard/tests/ghsa-wpj3-hf5j-x4v4.phpt new file mode 100644 index 00000000000..77fcb680894 --- /dev/null +++ b/ext/standard/tests/ghsa-wpj3-hf5j-x4v4.phpt @@ -0,0 +1,63 @@ +--TEST-- +ghsa-wpj3-hf5j-x4v4 (__Host-/__Secure- cookie bypass due to partial CVE-2022-31629 fix) +--COOKIE-- +..Host-test=ignore_1; +._Host-test=ignore_2; +.[Host-test=ignore_3; +_.Host-test=ignore_4; +__Host-test=ignore_5; +_[Host-test=ignore_6; +[.Host-test=ignore_7; +[_Host-test=ignore_8; +[[Host-test=ignore_9; +..Host-test[]=ignore_10; +._Host-test[]=ignore_11; +.[Host-test[]=ignore_12; +_.Host-test[]=ignore_13; +__Host-test[]=legitimate_14; +_[Host-test[]=legitimate_15; +[.Host-test[]=ignore_16; +[_Host-test[]=ignore_17; +[[Host-test[]=ignore_18; +..Secure-test=ignore_1; +._Secure-test=ignore_2; +.[Secure-test=ignore_3; +_.Secure-test=ignore_4; +__Secure-test=ignore_5; +_[Secure-test=ignore_6; +[.Secure-test=ignore_7; +[_Secure-test=ignore_8; +[[Secure-test=ignore_9; +..Secure-test[]=ignore_10; +._Secure-test[]=ignore_11; +.[Secure-test[]=ignore_12; +_.Secure-test[]=ignore_13; +__Secure-test[]=legitimate_14; +_[Secure-test[]=legitimate_15; +[.Secure-test[]=ignore_16; +[_Secure-test[]=ignore_17; +[[Secure-test[]=ignore_18; +--FILE-- + +--EXPECT-- +array(3) { + ["__Host-test"]=> + array(1) { + [0]=> + string(13) "legitimate_14" + } + ["_"]=> + array(2) { + ["Host-test["]=> + string(13) "legitimate_15" + ["Secure-test["]=> + string(13) "legitimate_15" + } + ["__Secure-test"]=> + array(1) { + [0]=> + string(13) "legitimate_14" + } +} diff --git a/main/php_variables.c b/main/php_variables.c index 17e4a1e5d2c..da7266416a5 100644 --- a/main/php_variables.c +++ b/main/php_variables.c @@ -54,6 +54,21 @@ static zend_always_inline void php_register_variable_quick(const char *name, siz zend_string_release_ex(key, 0); } +/* Discard variable if mangling made it start with __Host-, where pre-mangling it did not start with __Host- + * Discard variable if mangling made it start with __Secure-, where pre-mangling it did not start with __Secure- */ +static bool php_is_forbidden_variable_name(const char *mangled_name, size_t mangled_name_len, const char *pre_mangled_name) +{ + if (mangled_name_len >= sizeof("__Host-")-1 && strncmp(mangled_name, "__Host-", sizeof("__Host-")-1) == 0 && strncmp(pre_mangled_name, "__Host-", sizeof("__Host-")-1) != 0) { + return true; + } + + if (mangled_name_len >= sizeof("__Secure-")-1 && strncmp(mangled_name, "__Secure-", sizeof("__Secure-")-1) == 0 && strncmp(pre_mangled_name, "__Secure-", sizeof("__Secure-")-1) != 0) { + return true; + } + + return false; +} + PHPAPI void php_register_variable_ex(const char *var_name, zval *val, zval *track_vars_array) { char *p = NULL; @@ -104,20 +119,6 @@ PHPAPI void php_register_variable_ex(const char *var_name, zval *val, zval *trac } var_len = p - var; - /* Discard variable if mangling made it start with __Host-, where pre-mangling it did not start with __Host- */ - if (strncmp(var, "__Host-", sizeof("__Host-")-1) == 0 && strncmp(var_name, "__Host-", sizeof("__Host-")-1) != 0) { - zval_ptr_dtor_nogc(val); - free_alloca(var_orig, use_heap); - return; - } - - /* Discard variable if mangling made it start with __Secure-, where pre-mangling it did not start with __Secure- */ - if (strncmp(var, "__Secure-", sizeof("__Secure-")-1) == 0 && strncmp(var_name, "__Secure-", sizeof("__Secure-")-1) != 0) { - zval_ptr_dtor_nogc(val); - free_alloca(var_orig, use_heap); - return; - } - if (var_len==0) { /* empty variable name, or variable name with a space in it */ zval_ptr_dtor_nogc(val); free_alloca(var_orig, use_heap); @@ -221,6 +222,12 @@ PHPAPI void php_register_variable_ex(const char *var_name, zval *val, zval *trac return; } } else { + if (php_is_forbidden_variable_name(index, index_len, var_name)) { + zval_ptr_dtor_nogc(val); + free_alloca(var_orig, use_heap); + return; + } + gpc_element_p = zend_symtable_str_find(symtable1, index, index_len); if (!gpc_element_p) { zval tmp; @@ -258,6 +265,12 @@ plain_var: zval_ptr_dtor_nogc(val); } } else { + if (php_is_forbidden_variable_name(index, index_len, var_name)) { + zval_ptr_dtor_nogc(val); + free_alloca(var_orig, use_heap); + return; + } + zend_ulong idx; /* From 0ba5229a3f7572846e91c8f5382e87785f543826 Mon Sep 17 00:00:00 2001 From: Jakub Zelenka Date: Fri, 29 Mar 2024 15:27:59 +0000 Subject: [PATCH 3/7] Fix bug GHSA-q6x7-frmf-grcw: password_verify can erroneously return true Disallow null character in bcrypt password --- ext/standard/password.c | 5 +++++ ext/standard/tests/password/password_bcrypt_errors.phpt | 7 +++++++ 2 files changed, 12 insertions(+) diff --git a/ext/standard/password.c b/ext/standard/password.c index 651cffc9fe6..fbe58da6030 100644 --- a/ext/standard/password.c +++ b/ext/standard/password.c @@ -184,6 +184,11 @@ static zend_string* php_password_bcrypt_hash(const zend_string *password, zend_a zval *zcost; zend_long cost = PHP_PASSWORD_BCRYPT_COST; + if (memchr(ZSTR_VAL(password), '\0', ZSTR_LEN(password))) { + zend_value_error("Bcrypt password must not contain null character"); + return NULL; + } + if (options && (zcost = zend_hash_str_find(options, "cost", sizeof("cost")-1)) != NULL) { cost = zval_get_long(zcost); } diff --git a/ext/standard/tests/password/password_bcrypt_errors.phpt b/ext/standard/tests/password/password_bcrypt_errors.phpt index 10c3483f5a8..5d823cba021 100644 --- a/ext/standard/tests/password/password_bcrypt_errors.phpt +++ b/ext/standard/tests/password/password_bcrypt_errors.phpt @@ -14,7 +14,14 @@ try { } catch (ValueError $exception) { echo $exception->getMessage() . "\n"; } + +try { + var_dump(password_hash("null\0password", PASSWORD_BCRYPT)); +} catch (ValueError $e) { + echo $e->getMessage(), "\n"; +} ?> --EXPECT-- Invalid bcrypt cost parameter specified: 3 Invalid bcrypt cost parameter specified: 32 +Bcrypt password must not contain null character From de4f7f932166e66ff0950d97ae3bda5e355003d7 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Tue, 9 Apr 2024 23:41:29 -0500 Subject: [PATCH 4/7] Update NEWS --- NEWS | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/NEWS b/NEWS index 8373c66329a..0539611812d 100644 --- a/NEWS +++ b/NEWS @@ -2,6 +2,13 @@ PHP NEWS ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||| ?? ??? ????, PHP 8.1.28 +- Standard: + . Fixed bug GHSA-pc52-254m-w9w7 (Command injection via array-ish $command + parameter of proc_open). (CVE-2024-1874) (Jakub Zelenka) + . Fixed bug GHSA-wpj3-hf5j-x4v4 (__Host-/__Secure- cookie bypass due to + partial CVE-2022-31629 fix). (CVE-2024-2756) (nielsdos) + . Fixed bug GHSA-h746-cjrr-wfmr (password_verify can erroneously return true, + opening ATO risk). (CVE-2024-3096) (Jakub Zelenka) 21 Dec 2023, PHP 8.1.27 From 3394efc63e52a017995f92d8da4ef28224247bb3 Mon Sep 17 00:00:00 2001 From: Alex Dowad Date: Mon, 18 Mar 2024 23:23:28 +0200 Subject: [PATCH 5/7] Fix infinite loop in mb_encode_mimeheader --- ext/mbstring/mbstring.c | 55 +++++++++++-------- .../tests/mb_encode_mimeheader_basic4.phpt | 33 ++++++++++- 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/ext/mbstring/mbstring.c b/ext/mbstring/mbstring.c index cf4c1c32c59..618fff55362 100644 --- a/ext/mbstring/mbstring.c +++ b/ext/mbstring/mbstring.c @@ -5858,6 +5858,9 @@ static zend_string* mb_mime_header_encode(zend_string *input, const mbfl_encodin unsigned char *in = (unsigned char*)ZSTR_VAL(input); size_t in_len = ZSTR_LEN(input); + ZEND_ASSERT(outcode->mime_name != NULL); + ZEND_ASSERT(outcode->mime_name[0] != '\0'); + if (!in_len) { return zend_empty_string; } @@ -5880,7 +5883,8 @@ static zend_string* mb_mime_header_encode(zend_string *input, const mbfl_encodin unsigned int state = 0; /* wchar_buf should be big enough that when it is full, we definitely have enough * wchars to fill an entire line of output */ - uint32_t wchar_buf[80]; + const size_t wchar_buf_len = 90; + uint32_t wchar_buf[wchar_buf_len]; uint32_t *p, *e; /* What part of wchar_buf is filled with still-unprocessed data which should not * be overwritten? */ @@ -5891,7 +5895,7 @@ static zend_string* mb_mime_header_encode(zend_string *input, const mbfl_encodin * spaces), just pass it through unchanged */ bool checking_leading_spaces = true; while (in_len) { - size_t out_len = incode->to_wchar(&in, &in_len, wchar_buf, 80, &state); + size_t out_len = incode->to_wchar(&in, &in_len, wchar_buf, wchar_buf_len, &state); p = wchar_buf; e = wchar_buf + out_len; @@ -5925,9 +5929,9 @@ no_passthrough: ; * do so all the way to the end of the string */ while (in_len) { /* Decode part of the input string, refill wchar_buf */ - ZEND_ASSERT(offset < 80); - size_t out_len = incode->to_wchar(&in, &in_len, wchar_buf + offset, 80 - offset, &state); - ZEND_ASSERT(out_len <= 80 - offset); + ZEND_ASSERT(offset + MBSTRING_MIN_WCHAR_BUFSIZE <= wchar_buf_len); + size_t out_len = incode->to_wchar(&in, &in_len, wchar_buf + offset, wchar_buf_len - offset, &state); + ZEND_ASSERT(out_len <= wchar_buf_len - offset); p = wchar_buf; e = wchar_buf + offset + out_len; /* ASCII output is broken into space-delimited 'words' @@ -5948,6 +5952,7 @@ no_passthrough: ; * If we are already too far along on a line to include Base64/QPrint encoded data * on the same line (without overrunning max line length), then add a line feed * right now */ +feed_and_mime_encode: if (mb_convert_buf_len(&buf) - line_start + indent + strlen(outcode->mime_name) > 55) { MB_CONVERT_BUF_ENSURE(&buf, buf.out, buf.limit, (e - word_start) + linefeed_len + 1); buf.out = mb_convert_buf_appendn(buf.out, linefeed, linefeed_len); @@ -5985,7 +5990,13 @@ no_passthrough: ; if (in_len) { /* Copy chars which are part of an incomplete 'word' to the beginning - * of wchar_buf and reprocess them on the next iteration */ + * of wchar_buf and reprocess them on the next iteration. + * But first make sure that the incomplete 'word' isn't so big that + * there will be no space to add any more decoded wchars in the buffer + * (which could lead to an infinite loop) */ + if ((word_start - wchar_buf) < MBSTRING_MIN_WCHAR_BUFSIZE) { + goto feed_and_mime_encode; + } offset = e - word_start; if (offset) { memmove(wchar_buf, word_start, offset * sizeof(uint32_t)); @@ -6027,17 +6038,17 @@ mime_encoding_needed: ; /* Do we need to refill wchar_buf to make sure we don't run out of wchars * in the middle of a line? */ - if (p == wchar_buf) { + offset = e - p; + if (wchar_buf_len - offset < MBSTRING_MIN_WCHAR_BUFSIZE) { goto start_new_line; } - offset = e - p; memmove(wchar_buf, p, offset * sizeof(uint32_t)); while(true) { refill_wchar_buf: ; - ZEND_ASSERT(offset < 80); - size_t out_len = incode->to_wchar(&in, &in_len, wchar_buf + offset, 80 - offset, &state); - ZEND_ASSERT(out_len <= 80 - offset); + ZEND_ASSERT(offset + MBSTRING_MIN_WCHAR_BUFSIZE <= wchar_buf_len); + size_t out_len = incode->to_wchar(&in, &in_len, wchar_buf + offset, wchar_buf_len - offset, &state); + ZEND_ASSERT(out_len <= wchar_buf_len - offset); p = wchar_buf; e = wchar_buf + offset + out_len; @@ -6112,22 +6123,18 @@ start_new_line: ; indent = 0; /* Indent argument must only affect the first line */ - if (in_len) { - /* We still have more of input string remaining to decode */ + if (in_len || p < e) { + /* We still have more input to process */ buf.out = mb_convert_buf_appendn(buf.out, linefeed, linefeed_len); buf.out = mb_convert_buf_add(buf.out, ' '); line_start = mb_convert_buf_len(&buf); - /* Copy remaining wchars to beginning of buffer so they will be - * processed on the next iteration of outer 'do' loop */ offset = e - p; - memmove(wchar_buf, p, offset * sizeof(uint32_t)); - goto refill_wchar_buf; - } else if (p < e) { - /* Input string is finished, but we still have trailing wchars - * remaining to be processed in wchar_buf */ - buf.out = mb_convert_buf_appendn(buf.out, linefeed, linefeed_len); - buf.out = mb_convert_buf_add(buf.out, ' '); - line_start = mb_convert_buf_len(&buf); + if (in_len && (wchar_buf_len - offset >= MBSTRING_MIN_WCHAR_BUFSIZE)) { + /* Copy any remaining wchars to beginning of buffer and refill + * the rest of the buffer */ + memmove(wchar_buf, p, offset * sizeof(uint32_t)); + goto refill_wchar_buf; + } goto start_new_line; } else { /* We are done! */ @@ -6165,7 +6172,7 @@ PHP_FUNCTION(mb_encode_mimeheader) charset = php_mb_get_encoding(charset_name, 2); if (!charset) { RETURN_THROWS(); - } else if (charset->mime_name == NULL || charset->mime_name[0] == '\0') { + } else if (charset->mime_name == NULL || charset->mime_name[0] == '\0' || charset == &mbfl_encoding_qprint) { zend_argument_value_error(2, "\"%s\" cannot be used for MIME header encoding", ZSTR_VAL(charset_name)); RETURN_THROWS(); } diff --git a/ext/mbstring/tests/mb_encode_mimeheader_basic4.phpt b/ext/mbstring/tests/mb_encode_mimeheader_basic4.phpt index 7bf05b43ae3..d9a3407bf3f 100644 --- a/ext/mbstring/tests/mb_encode_mimeheader_basic4.phpt +++ b/ext/mbstring/tests/mb_encode_mimeheader_basic4.phpt @@ -115,11 +115,29 @@ var_dump(mb_encode_mimeheader("\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\ // In the general case, matching the old implementation's decision to transfer-encode or not // perfectly would require allocating potentially unbounded scratch memory (up to the size of // the input string), but we aim to only use a constant amount of temporarily allocated memory -var_dump(mb_encode_mimeheader("2\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20!3", "GB18030", "Q", "")); +var_dump(mb_encode_mimeheader("2\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20\x20!3", "GB18030", "Q", "")); + +// Regression test for infinite loop which was unintentionally caused when refactoring +var_dump(mb_encode_mimeheader(",9868949,9868978,9869015,9689100,9869121,9869615,9870690,9867116,98558119861183. ", "utf-8", "B")); +var_dump(mb_encode_mimeheader('xx ' . str_repeat("A", 81) . " ", "utf-8", "B")); + +// Regression test for problem where MIME encoding loop would not leave enough space in wchar +// buffer for the next iteration, causing an assertion failure +mb_internal_encoding('MacJapanese'); +var_dump(mb_encode_mimeheader("ne\xf6\xff\xff\xffs\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff1\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff1", 'CP50220', 'B', "A", 44)); + +// Regression test for failing assertion caused by the fact that QPrint deliberately generates no +// wchars for CR (0x0D) bytes +try { + mb_internal_encoding('Quoted-Printable'); + var_dump(mb_encode_mimeheader("=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=00=00=00=00=00=00=00=01=00=00=00=00=00=00=00850r=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=0D=00=00=00=0050r=08=0DCP850r850r0r", "Quoted-Printable", "B", "", 184)); +} catch (\ValueError $e) { + echo $e->getMessage() . \PHP_EOL; +} echo "Done"; ?> ---EXPECT-- +--EXPECTF-- string(0) "" string(21) "=?UTF-8?Q?abc=00abc?=" string(16) "=?UTF-8?B?Pw==?=" @@ -156,5 +174,14 @@ string(75) " 111111111111111111111111111111111111111111111111111111111111111111 string(33) "=?HZ-GB-2312?Q?=7E=7Bs=5B=7E=7D?=" string(77) "2 !3" string(282) "=?GB18030?Q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?GB18030?Q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?GB18030?Q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?GB18030?Q?=20=20=20=20=20=20=20=20=20=20=20=20!=33=20?=" -string(296) "2 =?GB18030?Q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?GB18030?Q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?GB18030?Q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?GB18030?Q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20!=33?=" +string(344) "2 =?GB18030?Q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?GB18030?Q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?GB18030?Q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?GB18030?Q?=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20=20?= =?GB18030?Q?=20=20=20=20=20=20=20=20=20!=33?=" +string(135) "=?UTF-8?B?LDk4Njg5NDksOTg2ODk3OCw5ODY5MDE1LDk2ODkxMDAsOTg2OTEyMSw5ODY5?= + =?UTF-8?B?NjE1LDk4NzA2OTAsOTg2NzExNiw5ODU1ODExOTg2MTE4My4g?=" +string(142) "xx =?UTF-8?B?QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB?= + =?UTF-8?B?QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBIA==?=" +string(690) "=?ISO-2022-JP?B?bmU/?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/cxskQiFEGyhCPw==?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/MRskQiFEGyhCPxskQiFEGyhCPw==?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/MQ==?=" + + +Deprecated: mb_encode_mimeheader(): Handling QPrint via mbstring is deprecated; use quoted_printable_encode/quoted_printable_decode instead in %s on line %d +mb_encode_mimeheader(): Argument #2 ($charset) "Quoted-Printable" cannot be used for MIME header encoding Done From c7c1336d0a3f3c1590c23315bf97b9778111b1e7 Mon Sep 17 00:00:00 2001 From: Niels Dossche <7771979+nielsdos@users.noreply.github.com> Date: Sat, 6 Apr 2024 23:32:35 +0200 Subject: [PATCH 6/7] Adapt regression test --- ext/mbstring/tests/mb_encode_mimeheader_basic4.phpt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ext/mbstring/tests/mb_encode_mimeheader_basic4.phpt b/ext/mbstring/tests/mb_encode_mimeheader_basic4.phpt index d9a3407bf3f..02206efab35 100644 --- a/ext/mbstring/tests/mb_encode_mimeheader_basic4.phpt +++ b/ext/mbstring/tests/mb_encode_mimeheader_basic4.phpt @@ -2,6 +2,8 @@ Test mb_encode_mimeheader() function : test cases found by fuzzer --EXTENSIONS-- mbstring +--INI-- +error_reporting=E_ALL^E_DEPRECATED --FILE-- ---EXPECTF-- +--EXPECT-- string(0) "" string(21) "=?UTF-8?Q?abc=00abc?=" string(16) "=?UTF-8?B?Pw==?=" @@ -180,8 +182,5 @@ string(135) "=?UTF-8?B?LDk4Njg5NDksOTg2ODk3OCw5ODY5MDE1LDk2ODkxMDAsOTg2OTEyMSw5O string(142) "xx =?UTF-8?B?QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB?= =?UTF-8?B?QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBIA==?=" string(690) "=?ISO-2022-JP?B?bmU/?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/cxskQiFEGyhCPw==?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/MRskQiFEGyhCPxskQiFEGyhCPw==?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/GyRCIUQbKEI/?=A =?ISO-2022-JP?B?GyRCIUQbKEI/GyRCIUQbKEI/MQ==?=" - - -Deprecated: mb_encode_mimeheader(): Handling QPrint via mbstring is deprecated; use quoted_printable_encode/quoted_printable_decode instead in %s on line %d mb_encode_mimeheader(): Argument #2 ($charset) "Quoted-Printable" cannot be used for MIME header encoding Done From 049082e4d0e9c4f2f91cd4b0d6362f4f767bb6b6 Mon Sep 17 00:00:00 2001 From: Ben Ramsey Date: Tue, 9 Apr 2024 23:53:27 -0500 Subject: [PATCH 7/7] Update NEWS --- NEWS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS b/NEWS index 6531e35b457..4a0af3c911c 100644 --- a/NEWS +++ b/NEWS @@ -117,6 +117,8 @@ PHP NEWS partial CVE-2022-31629 fix). (CVE-2024-2756) (nielsdos) . Fixed bug GHSA-h746-cjrr-wfmr (password_verify can erroneously return true, opening ATO risk). (CVE-2024-3096) (Jakub Zelenka) + . Fixed bug GHSA-fjp9-9hwx-59fq (mb_encode_mimeheader runs endlessly for some + inputs). (CVE-2024-2757) (Alex Dowad) 14 Mar 2024, PHP 8.3.4