CVE-2020-23352 (PHP, published 2021-01-27T16:15Z)
Description: 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.
Severity: MEDIUM (CVSS v3 base score: 7.5)
Weakness: NVD-CWE-Other \u2014 Other
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.
Repository: https://github.com/zblogcn/zblogphp
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 message: hash_equals函数添加及其应用;CloseTags函数进行了修正;


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) {