CVE-2021-4240: Use of Insufficiently Random Values (CWE-330)
value: A vulnerability, which was classified as problematic, was found in phpservermon. This affects the function generatePasswordResetToken of the file src/psm/Service/User.php. The manipulation leads to use of predictable algorithm in random number generator. The exploit has been disclosed to the public and may be used. The name of the patch is 3daa804d5f56c55b3ae13bfac368bb84ec632193. It is recommended to apply a patch to fix this issue. The identifier VDB-213717 was assigned to this vulnerability.
CWE description: The product uses insufficiently random numbers or values in a security context that depends on unpredictable numbers.
CVSS v3 base score: 5.3
Published: 2022-11-15T23:15Z
Repository: https://github.com/phpservermon/phpservermon
Language: PHP
Files changed: src/includes/psmconfig.inc.php, src/psm/Service/User.php, src/psm/Util/Install/Installer.php
Commit date: 2021-06-23 22:06:34 +0200
Commit message: SECURITY: Replaced mt_rand with random_bytes

https://huntr.dev/bounties/2-phpservermon/phpservermon/
CWE-1241: Use of Predictable Algorithm in Random Number Generator

Diff: diff --git a/src/includes/psmconfig.inc.php b/src/includes/psmconfig.inc.php
index 862ccda..9e190a6 100644
--- a/src/includes/psmconfig.inc.php
+++ b/src/includes/psmconfig.inc.php
@@ -28,11 +28,11 @@
  **/
 
 /**
  * Current PSM version
  */
-define('PSM_VERSION', '3.6.0.beta2');
+define('PSM_VERSION', '3.6.0');
 
 /**
  * URL to check for updates. Will not be checked if turned off on config page.
  * @see psm_update_available()
  */
diff --git a/src/psm/Service/User.php b/src/psm/Service/User.php
index 92979ed..fcd0718 100644
--- a/src/psm/Service/User.php
+++ b/src/psm/Service/User.php
@@ -70,11 +70,11 @@ class User
      * @var int $user_id
      */
     protected $user_id;
 
     /**
-     *Current user preferences
+     * Current user preferences
      * @var array $user_preferences
      */
     protected $user_preferences;
 
     /**
@@ -235,19 +235,19 @@ class User
         if (empty($user_name) && empty($user_password)) {
             return false;
         }
 
         $dirauthconfig = psm_get_conf('dirauth_status');
-        
+
         // LDAP auth enabled
         if ($dirauthconfig === '1') {
             $ldaplibpath = realpath(
                 PSM_PATH_SRC . '..' . DIRECTORY_SEPARATOR .
-                'vendor' . DIRECTORY_SEPARATOR .
-                'viharm' . DIRECTORY_SEPARATOR .
-                'psm-ldap-auth' . DIRECTORY_SEPARATOR .
-                'psmldapauth.php'
+                    'vendor' . DIRECTORY_SEPARATOR .
+                    'viharm' . DIRECTORY_SEPARATOR .
+                    'psm-ldap-auth' . DIRECTORY_SEPARATOR .
+                    'psmldapauth.php'
             );
             // If the library is found
             if ($ldaplibpath) {
                 // Delegate the authentication to the PsmLDAPauth module.
                 // If LDAP auth fails or if library not found, fall back to native auth
@@ -258,24 +258,24 @@ class User
 
         $user = $this->getUserByUsername($user_name);
 
         // Authenticated
         if ($ldapauthstatus === true) {
-          // Remove password to prevent it from being saved in the DB.
-          // Otherwise, user may still be authenticated if LDAP is disabled later.
-          $user_password = null;
-          @fn_Debug('Authenticated', $user);
+            // Remove password to prevent it from being saved in the DB.
+            // Otherwise, user may still be authenticated if LDAP is disabled later.
+            $user_password = null;
+            @fn_Debug('Authenticated', $user);
         } else {
 
-          // using PHP 5.5's password_verify() function to check if the provided passwords
-          // fits to the hash of that user's password
-          if (!isset($user->user_id)) {
-              password_verify($user_password, 'dummy_call_against_timing');
-              return false;
-          } elseif (!password_verify($user_password, $user->password)) {
-              return false;
-          }
+            // using PHP 5.5's password_verify() function to check if the provided passwords
+            // fits to the hash of that user's password
+            if (!isset($user->user_id)) {
+                password_verify($user_password, 'dummy_call_against_timing');
+                return false;
+            } elseif (!password_verify($user_password, $user->password)) {
+                return false;
+            }
         } // not authenticated
 
         $this->setUserLoggedIn($user->user_id, true);
 
         // if user has check the "remember me" checkbox, then generate token and write cookie
@@ -388,12 +388,12 @@ class User
         if ($user_id == 0) {
             return false;
         }
         // generate timestamp (to see when exactly the user (or an attacker) requested the password reset mail)
         $temporary_timestamp = time();
-        // generate random hash for email password reset verification (40 char string)
-        $user_password_reset_hash = sha1(uniqid(mt_rand(), true));
+        // generate random hash for email password reset verification (64 char string)
+        $user_password_reset_hash = hash('sha256', uniqid(random_bytes(64), true));
 
         $query_update = $this->db_connection->prepare('UPDATE ' .
             PSM_DB_PREFIX . 'users SET password_reset_hash = :user_password_reset_hash,
             password_reset_timestamp = :user_password_reset_timestamp
             WHERE user_id = :user_id');
diff --git a/src/psm/Util/Install/Installer.php b/src/psm/Util/Install/Installer.php
index c15b032..5728007 100644
--- a/src/psm/Util/Install/Installer.php
+++ b/src/psm/Util/Install/Installer.php
@@ -210,11 +210,11 @@ class Installer
             ) ENGINE=MyISAM DEFAULT CHARSET=utf8;",
             PSM_DB_PREFIX . 'users' => "CREATE TABLE IF NOT EXISTS `" . PSM_DB_PREFIX . "users` (
                 `user_id` int(11) unsigned NOT NULL AUTO_INCREMENT,
                 `user_name` varchar(64) NOT NULL COMMENT 'user''s name, unique',
                 `password` varchar(255) NOT NULL COMMENT 'user''s password in salted and hashed format',
-                `password_reset_hash` char(40) DEFAULT NULL COMMENT 'user''s password reset code',
+                `password_reset_hash` varchar(64) DEFAULT NULL COMMENT 'user''s password reset code',
                 `password_reset_timestamp` bigint(20) DEFAULT NULL COMMENT 'timestamp of the password reset request',
                 `rememberme_token` varchar(64) DEFAULT NULL COMMENT 'user''s remember-me cookie token',
                 `level` tinyint(2) unsigned NOT NULL DEFAULT '20',
                 `name` varchar(255) NOT NULL,
                 `mobile` varchar(15) NOT NULL,
@@ -228,11 +228,11 @@ class Installer
                 `email` varchar(255) NOT NULL,
                 PRIMARY KEY (`user_id`),
                 UNIQUE KEY `unique_username` (`user_name`)
             ) ENGINE=MyISAM DEFAULT CHARSET=utf8;",
             PSM_DB_PREFIX .
-            'users_preferences' => "CREATE TABLE IF NOT EXISTS `" . PSM_DB_PREFIX . "users_preferences` (
+                'users_preferences' => "CREATE TABLE IF NOT EXISTS `" . PSM_DB_PREFIX . "users_preferences` (
                 `user_id` int(11) unsigned NOT NULL,
                 `key` varchar(255) NOT NULL,
                 `value` varchar(255) NOT NULL,
                 PRIMARY KEY (`user_id`, `key`)
             ) ENGINE=MyISAM DEFAULT CHARSET=utf8;",
@@ -730,10 +730,11 @@ class Installer
     }
 
     /**
      * Patch for v3.6.0 release
      * Added support for Discord and webhooks
+     * Password_reset_hash varchar 40 -> 64 to allow for SHA256 hash
      */
     protected function upgrade360()
     {
         $queries = array();
 
@@ -756,10 +757,12 @@ class Installer
         $queries[] = "ALTER TABLE `" . PSM_DB_PREFIX . "servers` 
             ADD `discord` ENUM( 'yes','no' ) NOT NULL DEFAULT 'yes' AFTER  `sms`;";
         $queries[] = "INSERT INTO `" . PSM_DB_PREFIX . "users` (
             `user_name`, `level`, `name`, `email`)
             VALUES ('__PUBLIC__', 30, 'Public page', 'publicpage@psm.psm')";
+        $queries[] = "ALTER TABLE `" . PSM_DB_PREFIX . "users`
+            CHANGE `password_reset_hash` `password_reset_hash`  VARCHAR( 64 ) DEFAULT NULL COMMENT 'user''s password reset code';";
         $this->execSQL($queries);
 
         $this->log('Public page is now available. Added user \'__PUBLIC__\'. See documentation for more info.');
     }
 }

Vulnerable code: define('PSM_VERSION', '3.6.0.beta2');
     *Current user preferences
        
                'vendor' . DIRECTORY_SEPARATOR .
                'viharm' . DIRECTORY_SEPARATOR .
                'psm-ldap-auth' . DIRECTORY_SEPARATOR .
                'psmldapauth.php'
          // Remove password to prevent it from being saved in the DB.
          // Otherwise, user may still be authenticated if LDAP is disabled later.
          $user_password = null;
          @fn_Debug('Authenticated', $user);
          // using PHP 5.5's password_verify() function to check if the provided passwords
          // fits to the hash of that user's password
          if (!isset($user->user_id)) {
              password_verify($user_password, 'dummy_call_against_timing');
              return false;
          } elseif (!password_verify($user_password, $user->password)) {
              return false;
          }
        // generate random hash for email password reset verification (40 char string)
        $user_password_reset_hash = sha1(uniqid(mt_rand(), true));
                `password_reset_hash` char(40) DEFAULT NULL COMMENT 'user''s password reset code',
            'users_preferences' => "CREATE TABLE IF NOT EXISTS `" . PSM_DB_PREFIX . "users_preferences` (
Fixed code: define('PSM_VERSION', '3.6.0');
     * Current user preferences

                    'vendor' . DIRECTORY_SEPARATOR .
                    'viharm' . DIRECTORY_SEPARATOR .
                    'psm-ldap-auth' . DIRECTORY_SEPARATOR .
                    'psmldapauth.php'
            // Remove password to prevent it from being saved in the DB.
            // Otherwise, user may still be authenticated if LDAP is disabled later.
            $user_password = null;
            @fn_Debug('Authenticated', $user);
            // using PHP 5.5's password_verify() function to check if the provided passwords
            // fits to the hash of that user's password
            if (!isset($user->user_id)) {
                password_verify($user_password, 'dummy_call_against_timing');
                return false;
            } elseif (!password_verify($user_password, $user->password)) {
                return false;
            }
        // generate random hash for email password reset verification (64 char string)
        $user_password_reset_hash = hash('sha256', uniqid(random_bytes(64), true));
                `password_reset_hash` varchar(64) DEFAULT NULL COMMENT 'user''s password reset code',
                'users_preferences' => "CREATE TABLE IF NOT EXISTS `" . PSM_DB_PREFIX . "users_preferences` (
     * Password_reset_hash varchar 40 -> 64 to allow for SHA256 hash
        $queries[] = "ALTER TABLE `" . PSM_DB_PREFIX . "users`
            CHANGE `password_reset_hash` `password_reset_hash`  VARCHAR( 64 ) DEFAULT NULL COMMENT 'user''s password reset code';";