CVE-2020-23352: Other (NVD-CWE-Other)
value: Z-BlogPHP 1.6.0 Valyria is affected by incorrect access control. PHP loose comparison and a magic hash can be used to bypass authentication. zb_user/plugin/passwordvisit/include.php:passwordvisit_input_password() uses loose comparison to authenticate, which can be bypassed via magic hash values.
CWE description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
CVSS v3 base score: 7.5
Published: 2021-01-27T16:15Z
Repository: https://github.com/zblogcn/zblogphp
Language: PHP
Files changed: phpcs.xml, zb_system/function/c_system_common.php, zb_system/function/c_system_compat.php, zb_system/function/lib/zblogphp.php, zb_users/plugin/passwordvisit/include.php
Commit date: 2020-05-21 17:54:45 +0800
Commit message: hash_equals函数添加及其应用;CloseTags函数进行了修正;

Diff: diff --git a/phpcs.xml b/phpcs.xml
index f3366bb4..7cde0fda 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -40,10 +40,11 @@
 		<exclude name="PHPCompatibility.FunctionUse.NewFunctions.fnmatch.Found" />
 		<exclude name="PHPCompatibility.FunctionUse.NewFunctions.http_build_url.Found" />
 		<exclude name="PHPCompatibility.FunctionUse.NewFunctions.rrmdir.Found" />
 		<exclude name="PHPCompatibility.FunctionUse.NewFunctions.gzdecode.Found" />
 		<exclude name="PHPCompatibility.FunctionUse.NewFunctions.array_replace_recursive.Found" />
+		<exclude name="PHPCompatibility.FunctionUse.NewFunctions.hash_equals.Found" />
 		<exclude name="PHPCompatibility.Constants.NewConstants.PHP_VERSION_ID.Found" />
 		<exclude name="PHPCompatibility.Constants.NewConstants.FNM_PATHNAME.Found" />
 		<exclude name="PHPCompatibility.Constants.NewConstants.FNM_NOESCAPE.Found" />
 		<exclude name="PHPCompatibility.Constants.NewConstants.FNM_PERIOD.Found" />
 		<exclude name="PHPCompatibility.Constants.NewConstants.FNM_CASEFOLD.Found" />
diff --git a/zb_system/function/c_system_common.php b/zb_system/function/c_system_common.php
index 7f63ae49..0e563224 100644
--- a/zb_system/function/c_system_common.php
+++ b/zb_system/function/c_system_common.php
@@ -1216,46 +1216,26 @@ function TransferHTML($source, $param)
  *
  * @return string
  */
 function CloseTags($html)
 {
-    // strip fraction of open or close tag from end (e.g. if we take first x characters, we might cut off a tag at the end!)
-    $html = preg_replace('/<[^>]*$/', '', $html); // ending with fraction of open tag
-
-    // put open tags into an array
-    preg_match_all('#<([a-z]+)(?: .*)?(?<![/|/ ])>#iU', $html, $result);
-    $opentags = $result[1];
-
-    // put all closed tags into an array
+    preg_match_all('#<(?!meta|img|br|hr|input\b)\b([a-z]+)(?: .*)?(?<![/|/ ])>#iU', $html, $result);
+    $openedtags = $result[1];
     preg_match_all('#</([a-z]+)>#iU', $html, $result);
-    $closetags = $result[1];
-
-    $len_opened = count($opentags);
-
-    // if all tags are closed, we can return
-    if (count($closetags) == $len_opened) {
+    $closedtags = $result[1];
+    $len_opened = count($openedtags);
+    if (count($closedtags) == $len_opened) {
         return $html;
     }
-
-    // close tags in reverse order that they were opened
-    $opentags = array_reverse($opentags);
-
-    // self closing tags
-    $sc = array('br', 'input', 'img', 'hr', 'meta', 'link');
-    // ,'frame','iframe','param','area','base','basefont','col'
-    // should not skip tags that can have content inside!
-
-    for ($i = 0; $i < $len_opened; $i++) {
-        $ot = strtolower($opentags[$i]);
-
-        if (!in_array($opentags[$i], $closetags) && !in_array($ot, $sc)) {
-            $html .= '</' . $opentags[$i] . '>';
+    $openedtags = array_reverse($openedtags);
+    for ($i=0; $i < $len_opened; $i++) {
+        if (!in_array($openedtags[$i], $closedtags)) {
+            $html .= '</'.$openedtags[$i].'>';
         } else {
-            unset($closetags[array_search($opentags[$i], $closetags)]);
+            unset($closedtags[array_search($openedtags[$i], $closedtags)]);
         }
     }
-
     return $html;
 }
 
 /**
  * 获取UTF8格式的字符串的子串.
diff --git a/zb_system/function/c_system_compat.php b/zb_system/function/c_system_compat.php
index ad5b7dec..4a6f8c22 100644
--- a/zb_system/function/c_system_compat.php
+++ b/zb_system/function/c_system_compat.php
@@ -444,10 +444,59 @@ if (!function_exists('array_replace_recursive')) {
         for ($i = 1; $i < count($args); $i++) {
             if (is_array($args[$i])) {
                 $array = recurse($array, $args[$i]);
             }
         }
-
         return $array;
     }
 
 }
+
+
+if (!function_exists('hash_equals')) {
+
+    /**
+     * Timing attack safe string comparison
+     *
+     * Compares two strings using the same time whether they're equal or not.
+     * This function should be used to mitigate timing attacks; for instance, when testing crypt() password hashes.
+     *
+     * @param string $known_string The string of known length to compare against
+     * @param string $user_string The user-supplied string
+     * @return boolean Returns TRUE when the two strings are equal, FALSE otherwise.
+     */
+    function hash_equals($known_string, $user_string)
+    {
+        if (func_num_args() !== 2) {
+            // handle wrong parameter count as the native implentation
+            trigger_error('hash_equals() expects exactly 2 parameters, ' . func_num_args() . ' given', E_USER_WARNING);
+            return null;
+        }
+        if (is_string($known_string) !== true) {
+            trigger_error('hash_equals(): Expected known_string to be a string, ' . gettype($known_string) . ' given', E_USER_WARNING);
+            return false;
+        }
+        $known_string_len = strlen($known_string);
+        $user_string_type_error = 'hash_equals(): Expected user_string to be a string, ' . gettype($user_string) . ' given'; // prepare wrong type error message now to reduce the impact of string concatenation and the gettype call
+        if (is_string($user_string) !== true) {
+            trigger_error($user_string_type_error, E_USER_WARNING);
+            // prevention of timing attacks might be still possible if we handle $user_string as a string of diffent length (the trigger_error() call increases the execution time a bit)
+            $user_string_len = strlen($user_string);
+            $user_string_len = $known_string_len + 1;
+        } else {
+            $user_string_len = $known_string_len + 1;
+            $user_string_len = strlen($user_string);
+        }
+        if ($known_string_len !== $user_string_len) {
+            $res = $known_string ^ $known_string; // use $known_string instead of $user_string to handle strings of diffrent length.
+            $ret = 1; // set $ret to 1 to make sure false is returned
+        } else {
+            $res = $known_string ^ $user_string;
+            $ret = 0;
+        }
+        for ($i = strlen($res) - 1; $i >= 0; $i--) {
+            $ret |= ord($res[$i]);
+        }
+        return $ret === 0;
+    }
+
+}
diff --git a/zb_system/function/lib/zblogphp.php b/zb_system/function/lib/zblogphp.php
index e0e82b03..b9172ea3 100644
--- a/zb_system/function/lib/zblogphp.php
+++ b/zb_system/function/lib/zblogphp.php
@@ -1314,11 +1314,11 @@ class ZBlogPHP
         if ($name == '' || $password == '') {
             return false;
         }
         $m = $this->GetMemberByName($name);
         if ($m->ID != null) {
-            if (strcasecmp($m->Password, $password) == 0) {
+            if (hash_equals($m->Password, $password) === true) {
                 $member = $m;
 
                 return true;
             }
         }
diff --git a/zb_users/plugin/passwordvisit/include.php b/zb_users/plugin/passwordvisit/include.php
index 31a38e54..2ce78d5d 100644
--- a/zb_users/plugin/passwordvisit/include.php
+++ b/zb_users/plugin/passwordvisit/include.php
@@ -29,11 +29,11 @@ function passwordvisit_input_password(&$template)
 {
     global $zbp;
     if (isset($_POST['password']) && $_POST['password'] != '') {
         $article = $template->GetTags('article');
         if ($article->Metas->passwordvisit_password != '') {
-            if (GetVars('password', 'POST') == $article->Metas->passwordvisit_password) {
+            if (GetVars('password', 'POST') === $article->Metas->passwordvisit_password) {
                 return;
             } else {
                 echo '<script type="text/javascript">alert("密码错误，请重新输入！");window.location="' . $article->Url . '";</script>';
                 die();
             }

Vulnerable code:     // strip fraction of open or close tag from end (e.g. if we take first x characters, we might cut off a tag at the end!)
    $html = preg_replace('/<[^>]*$/', '', $html); // ending with fraction of open tag

    // put open tags into an array
    preg_match_all('#<([a-z]+)(?: .*)?(?<![/|/ ])>#iU', $html, $result);
    $opentags = $result[1];

    // put all closed tags into an array
    $closetags = $result[1];

    $len_opened = count($opentags);

    // if all tags are closed, we can return
    if (count($closetags) == $len_opened) {

    // close tags in reverse order that they were opened
    $opentags = array_reverse($opentags);

    // self closing tags
    $sc = array('br', 'input', 'img', 'hr', 'meta', 'link');
    // ,'frame','iframe','param','area','base','basefont','col'
    // should not skip tags that can have content inside!

    for ($i = 0; $i < $len_opened; $i++) {
        $ot = strtolower($opentags[$i]);

        if (!in_array($opentags[$i], $closetags) && !in_array($ot, $sc)) {
            $html .= '</' . $opentags[$i] . '>';
            unset($closetags[array_search($opentags[$i], $closetags)]);


            if (strcasecmp($m->Password, $password) == 0) {
            if (GetVars('password', 'POST') == $article->Metas->passwordvisit_password) {
Fixed code: 		<exclude name="PHPCompatibility.FunctionUse.NewFunctions.hash_equals.Found" />
    preg_match_all('#<(?!meta|img|br|hr|input\b)\b([a-z]+)(?: .*)?(?<![/|/ ])>#iU', $html, $result);
    $openedtags = $result[1];
    $closedtags = $result[1];
    $len_opened = count($openedtags);
    if (count($closedtags) == $len_opened) {
    $openedtags = array_reverse($openedtags);
    for ($i=0; $i < $len_opened; $i++) {
        if (!in_array($openedtags[$i], $closedtags)) {
            $html .= '</'.$openedtags[$i].'>';
            unset($closedtags[array_search($openedtags[$i], $closedtags)]);


if (!function_exists('hash_equals')) {

    /**
     * Timing attack safe string comparison
     *
     * Compares two strings using the same time whether they're equal or not.
     * This function should be used to mitigate timing attacks; for instance, when testing crypt() password hashes.
     *
     * @param string $known_string The string of known length to compare against
     * @param string $user_string The user-supplied string
     * @return boolean Returns TRUE when the two strings are equal, FALSE otherwise.
     */
    function hash_equals($known_string, $user_string)
    {
        if (func_num_args() !== 2) {
            // handle wrong parameter count as the native implentation
            trigger_error('hash_equals() expects exactly 2 parameters, ' . func_num_args() . ' given', E_USER_WARNING);
            return null;
        }
        if (is_string($known_string) !== true) {
            trigger_error('hash_equals(): Expected known_string to be a string, ' . gettype($known_string) . ' given', E_USER_WARNING);
            return false;
        }
        $known_string_len = strlen($known_string);
        $user_string_type_error = 'hash_equals(): Expected user_string to be a string, ' . gettype($user_string) . ' given'; // prepare wrong type error message now to reduce the impact of string concatenation and the gettype call
        if (is_string($user_string) !== true) {
            trigger_error($user_string_type_error, E_USER_WARNING);
            // prevention of timing attacks might be still possible if we handle $user_string as a string of diffent length (the trigger_error() call increases the execution time a bit)
            $user_string_len = strlen($user_string);
            $user_string_len = $known_string_len + 1;
        } else {
            $user_string_len = $known_string_len + 1;
            $user_string_len = strlen($user_string);
        }
        if ($known_string_len !== $user_string_len) {
            $res = $known_string ^ $known_string; // use $known_string instead of $user_string to handle strings of diffrent length.
            $ret = 1; // set $ret to 1 to make sure false is returned
        } else {
            $res = $known_string ^ $user_string;
            $ret = 0;
        }
        for ($i = strlen($res) - 1; $i >= 0; $i--) {
            $ret |= ord($res[$i]);
        }
        return $ret === 0;
    }

}
            if (hash_equals($m->Password, $password) === true) {
            if (GetVars('password', 'POST') === $article->Metas->passwordvisit_password) {