CVE-2022-27157: Weak Password Recovery Mechanism for Forgotten Password (CWE-640)
value: pearweb < 1.32 is suffers from a Weak Password Recovery Mechanism via include/users/passwordmanage.php.
CWE description: The product contains a mechanism for users to recover or change their passwords without knowing the original password, but the mechanism is weak.
CVSS v3 base score: 9.8
Published: 2022-04-15T18:15Z
Repository: https://github.com/pear/pearweb
Language: PHP
Files changed: include/bugs/pear-bug-accountrequest.php, include/pear-database-user.php, include/pepr/pepr-ppvote.php, include/users/passwordmanage.php, pearweb.php, pearweb_election.php, pearweb_pepr.php, public_html/admin/index.php, rollback.php, scripts/rollbackProposal.php
Commit date: 2022-03-13 09:07:46 -0500
Commit message: Be cautious about what can be unserialized

Diff: diff --git a/include/bugs/pear-bug-accountrequest.php b/include/bugs/pear-bug-accountrequest.php
index c60df878..8d65a165 100644
--- a/include/bugs/pear-bug-accountrequest.php
+++ b/include/bugs/pear-bug-accountrequest.php
@@ -234,11 +234,15 @@ class PEAR_Bug_Accountrequest
         include_once 'pear-database-user.php';
         $user = user::info($handle, null, 0);
         if (!isset($user['registered'])) {
             return false;
         }
-        @$arr = unserialize($user['userinfo']);
+        try {
+            @$arr = unserialize($user['userinfo'], ['allowed_classes' => false]);
+        } catch (Exception $ex) {
+            $arr = false;
+        }
 
         include_once 'pear-database-note.php';
         note::removeAll($handle);
 
         $data = array();
diff --git a/include/pear-database-user.php b/include/pear-database-user.php
index bc1d3a13..14dcdfb0 100644
--- a/include/pear-database-user.php
+++ b/include/pear-database-user.php
@@ -44,11 +44,15 @@ class user
 
         $user = user::info($uid, null, 0);
         if (!isset($user['registered'])) {
             return false;
         }
-        @$arr = unserialize($user['userinfo']);
+        try {
+            @$arr = unserialize($user['userinfo'], ['allowed_classes' => false]);
+        } catch (Exception $ex) {
+            $arr = false;
+        }
 
         include_once 'pear-database-note.php';
         note::removeAll($uid);
 
         $data = array();
diff --git a/include/pepr/pepr-ppvote.php b/include/pepr/pepr-ppvote.php
index 436c4dd1..4974da16 100644
--- a/include/pepr/pepr-ppvote.php
+++ b/include/pepr/pepr-ppvote.php
@@ -55,11 +55,18 @@ class ppVote
         }
         if (!$res->numRows()) {
             return null;
         }
         $set = $res->fetchRow(DB_FETCHMODE_ASSOC);
-        $set['reviews'] = unserialize($set['reviews']);
+        try {
+            $unserialised = unserialize($set['reviews'], ['allowed_classes' => false]);
+            if ($unserialised !== false) {
+                $set['reviews'] = $unserialised;
+            }
+        } catch (Exception $ex) {
+            $set['reviews'] = array();
+        }
         $vote = new ppVote($set);
         return $vote;
     }
 
     function &getAll(&$dbh, $proposalId)
@@ -69,11 +76,14 @@ class ppVote
         if (DB::isError($res)) {
             return $res;
         }
         $votes = array();
         while ($set = $res->fetchRow(DB_FETCHMODE_ASSOC)) {
-            $set['reviews'] = unserialize($set['reviews']);
+            $uReviews = unserialize($set['reviews'], ['allowed_classes' => false]);
+            if ($uReviews !== false) {
+                $set['reviews'] = $uReviews;
+            }
             $votes[$set['user_handle']] = new ppVote($set);
         }
         return $votes;
     }
 
diff --git a/include/users/passwordmanage.php b/include/users/passwordmanage.php
index 9fcc3825..84d55b73 100644
--- a/include/users/passwordmanage.php
+++ b/include/users/passwordmanage.php
@@ -53,11 +53,16 @@ class Users_PasswordManage
      */
     function resetPassword($user, $pass1, $pass2)
     {
         require_once 'Damblan/Mailer.php';
         $errors = array();
-        $salt = md5(mt_rand(4,13) . $user . time() . $pass1);
+        $random_bytes = openssl_random_pseudo_bytes(16, $strong);
+        if ($random_bytes === false || $strong === false) {
+            $errors[] = "Could not generate a safe password token";
+            return $errors;
+        }
+        $salt = md5($rand_bytes);
         PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
         $this->_dbh->query('DELETE FROM lostpassword WHERE handle=?', array($user));
         $e = $this->_dbh->query('INSERT INTO lostpassword
             (handle, newpassword, salt, requested)
             VALUES(?,?,?,NOW())', array($user, md5($pass1), $salt));
@@ -89,6 +94,6 @@ purged after 24 hours.
 PEAR Quality Assurance.'), array('username' => $user, 'salt' => $salt));
             $this->_mailer->send();
         }
         return $errors;
     }
-}
\ No newline at end of file
+}
diff --git a/pearweb.php b/pearweb.php
index 85438d21..00cafb5a 100644
--- a/pearweb.php
+++ b/pearweb.php
@@ -143,12 +143,17 @@ class pearweb_postinstall
             // we would uncomment the following line:
             //$c['overwrite'] = true;
             $oldversion = false;
         }
         if ($oldversion) {
-            $curdef = unserialize(file_get_contents($updir .
-              $answers['database'] . '-' . $oldversion . '.ser'));
+            $sFile = $updir . $answers['database'] . '-' . $oldversion . '.ser';
+            try {
+                $curdef = unserialize(file_get_contents($sFile), ['allowed_classes' => false]);
+            } catch (Exception $ex) {
+                $curdef = false;
+            }
+
             if (!is_array($curdef)) {
                 $this->_ui->outputData('invalid data returned from previous version');
             }
             // get a database diff (MDB2_Schema is very useful here)
             PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
@@ -353,6 +358,6 @@ class pearweb_postinstall
         fwrite($fp, implode('', $httpdconf));
         fclose($fp);
         $this->_ui->outputData('...done');
         return true;
     }
-}
\ No newline at end of file
+}
diff --git a/pearweb_election.php b/pearweb_election.php
index 9a69a0ba..dcea3af5 100644
--- a/pearweb_election.php
+++ b/pearweb_election.php
@@ -136,12 +136,16 @@ class pearweb_election_postinstall
             // we would uncomment the following line:
             //$c['overwrite'] = true;
             $oldversion = false;
         }
         if ($oldversion) {
-            $curdef = unserialize(file_get_contents('@www-dir@/sql/.pearweb-upgrade/' .
-              $answers['database'] . '-' . $oldversion . '.ser'));
+            $sFile = '@www-dir@/sql/.pearweb-upgrade/' . $answers['database'] . '-' . $oldversion . '.ser';
+            try {
+                $curdef = unserialize(file_get_contents($sFile), ['allowed_classes' => false]);
+            } catch (Exception $ex) {
+                $curdef = false;
+            }
             if (!is_array($curdef)) {
                 $this->_ui->outputData('invalid data returned from previous version');
             }
             // get a database diff (MDB2_Schema is very useful here)
             PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
@@ -164,6 +168,6 @@ class pearweb_election_postinstall
             $this->_ui->outputData($err->getMessage());
             return false;
         }
         return true;
     }
-}
\ No newline at end of file
+}
diff --git a/pearweb_pepr.php b/pearweb_pepr.php
index c00a2d7b..67ce71c3 100644
--- a/pearweb_pepr.php
+++ b/pearweb_pepr.php
@@ -143,12 +143,16 @@ class pearweb_pepr_postinstall
             // we would uncomment the following line:
             //$c['overwrite'] = true;
             $oldversion = false;
         }
         if ($oldversion) {
-            $curdef = unserialize(file_get_contents('@www-dir@/sql/.pearweb-upgrade/' .
-              $answers['database'] . '-' . $oldversion . '.ser'));
+            try {
+                $sFile = '@www-dir@/sql/.pearweb-upgrade/' . $answers['database'] . '-' . $oldversion . '.ser';
+                $curdef = unserialize(file_get_contents($sFile), ['allowed_classes' => false]);
+            } catch (Exception $ex) {
+                $curdef = false;
+            }
             if (!is_array($curdef)) {
                 $this->_ui->outputData('invalid data returned from previous version');
             }
             // get a database diff (MDB2_Schema is very useful here)
             PEAR::staticPushErrorHandling(PEAR_ERROR_RETURN);
@@ -320,6 +324,6 @@ class pearweb_pepr_postinstall
         fwrite($fp, implode('', $httpdconf));
         fclose($fp);
         $this->_ui->outputData('...done');
         return true;
     }
-}
\ No newline at end of file
+}
diff --git a/public_html/admin/index.php b/public_html/admin/index.php
index 385bd358..fda9f524 100755
--- a/public_html/admin/index.php
+++ b/public_html/admin/index.php
@@ -265,11 +265,19 @@ do {
         include_once 'pear-database-user.php';
         $requser = user::info($acreq, null, false);
         if (empty($requser['name']) || $requser['from_site'] == 'pecl') {
             break;
         }
-        list($purpose, $moreinfo) = @unserialize($requser['userinfo']);
+        try {
+            $uInfo = @unserialize($requser['userinfo'], ['allowed_classes' => false]);
+            if ($uInfo !== false) {
+                list($purpose, $moreinfo) = $uInfo;
+            }
+        } catch (Exception $ex) {
+            $purpose = 'n/a';
+            $moreinfo = 'n/a';
+        }
 
         $bb = new BorderBox('Account request from ' . $requser['name'] . ' &lt;' . $requser['email'] . '&gt;', "100%", '', 2, true);
         $bb->horizHeadRow("Requested username:", $requser['handle']);
         $bb->horizHeadRow("Realname:", $requser['name']);
         $bb->horizHeadRow("Email address:", '<a href="mailto:' . $requser['email'] . '">' . $requser['email'] . "</a>");
@@ -509,12 +517,12 @@ foreach ($reasons as $reason) {
 
         foreach ($requests as $handle => $data) {
             list($name, $note, $userinfo) = $data;
 
                 // Grab userinfo/request purpose
-            if (@unserialize($userinfo)) {
-                $userinfo = @unserialize($userinfo);
+            if (@unserialize($userinfo, ['allowed_classes' => false])) {
+                $userinfo = @unserialize($userinfo, ['allowed_classes' => false]);
                 $account_purpose = $userinfo[0];
             } else {
                 $account_purpose = $userinfo;
             }
 
diff --git a/rollback.php b/rollback.php
index 684937c1..94a2dd31 100755
--- a/rollback.php
+++ b/rollback.php
@@ -72,11 +72,20 @@ class Pearweb_Rollback
         while ($row = $res->fetchRow(MDB2_FETCHMODE_OBJECT)) {
 
             $comment  = "Original vote: {$row->value}\n";
             $comment .= "Conditional vote: " . ($row->is_conditional != 0)?'yes':'no' . "\n";
             $comment .= "Comment on vote: " . $row->comment . "\n\n";
-            $comment .= "Reviewed: " . implode(", ", unserialize($row->reviews));
+            $reviewed = "Reviewed: n/a";
+            try {
+                $uInfo = unserialize($row->reviews, ['allowed_classes' => false]);
+                if ($uInfo !== false) {
+                    $reviewed = "Reviewed: " . implode(", ", $uInfo);
+                }
+            } catch (Exception $ex) {
+                // do nothing
+            }
+            $comment .= $reviewed;
 
             $sql = sprintf(
                 $insert,
                 $this->mdb2->quote($row->user_handle),
                 $row->timestamp,
diff --git a/scripts/rollbackProposal.php b/scripts/rollbackProposal.php
index ca55a00b..c792a8d6 100644
--- a/scripts/rollbackProposal.php
+++ b/scripts/rollbackProposal.php
@@ -72,11 +72,20 @@ class Pearweb_Rollback
         while ($row = $res->fetchRow(MDB2_FETCHMODE_OBJECT)) {
 
             $comment  = "Original vote: {$row->value}\n";
             $comment .= "Conditional vote: " . (($row->is_conditional != 0)?'yes':'no') . "\n";
             $comment .= "Comment on vote: " . $row->comment . "\n";
-            $comment .= "Reviewed: " . implode(", ", unserialize($row->reviews));
+            $reviewed = "Reviewed: n/a";
+            try {
+                $uInfo = unserialize($row->reviews, ['allowed_classes' => false]);
+                if ($uInfo !== false) {
+                    $reviewed = "Reviewed: " . implode(", ", $uInfo);
+                }
+            } catch (Exception $ex) {
+                // do nothing
+            }
+            $comment .= $reviewed;
 
             $sql = sprintf(
                 $insert,
                 $this->mdb2->quote($row->user_handle),
                 $row->timestamp,

Vulnerable code:         @$arr = unserialize($user['userinfo']);
        @$arr = unserialize($user['userinfo']);
        $set['reviews'] = unserialize($set['reviews']);
            $set['reviews'] = unserialize($set['reviews']);
        $salt = md5(mt_rand(4,13) . $user . time() . $pass1);
}
            $curdef = unserialize(file_get_contents($updir .
              $answers['database'] . '-' . $oldversion . '.ser'));
}
            $curdef = unserialize(file_get_contents('@www-dir@/sql/.pearweb-upgrade/' .
              $answers['database'] . '-' . $oldversion . '.ser'));
}
            $curdef = unserialize(file_get_contents('@www-dir@/sql/.pearweb-upgrade/' .
              $answers['database'] . '-' . $oldversion . '.ser'));
}
        list($purpose, $moreinfo) = @unserialize($requser['userinfo']);
            if (@unserialize($userinfo)) {
                $userinfo = @unserialize($userinfo);
            $comment .= "Reviewed: " . implode(", ", unserialize($row->reviews));
            $comment .= "Reviewed: " . implode(", ", unserialize($row->reviews));
Fixed code:         try {
            @$arr = unserialize($user['userinfo'], ['allowed_classes' => false]);
        } catch (Exception $ex) {
            $arr = false;
        }
        try {
            @$arr = unserialize($user['userinfo'], ['allowed_classes' => false]);
        } catch (Exception $ex) {
            $arr = false;
        }
        try {
            $unserialised = unserialize($set['reviews'], ['allowed_classes' => false]);
            if ($unserialised !== false) {
                $set['reviews'] = $unserialised;
            }
        } catch (Exception $ex) {
            $set['reviews'] = array();
        }
            $uReviews = unserialize($set['reviews'], ['allowed_classes' => false]);
            if ($uReviews !== false) {
                $set['reviews'] = $uReviews;
            }
        $random_bytes = openssl_random_pseudo_bytes(16, $strong);
        if ($random_bytes === false || $strong === false) {
            $errors[] = "Could not generate a safe password token";
            return $errors;
        }
        $salt = md5($rand_bytes);
}
            $sFile = $updir . $answers['database'] . '-' . $oldversion . '.ser';
            try {
                $curdef = unserialize(file_get_contents($sFile), ['allowed_classes' => false]);
            } catch (Exception $ex) {
                $curdef = false;
            }

}
            $sFile = '@www-dir@/sql/.pearweb-upgrade/' . $answers['database'] . '-' . $oldversion . '.ser';
            try {
                $curdef = unserialize(file_get_contents($sFile), ['allowed_classes' => false]);
            } catch (Exception $ex) {
                $curdef = false;
            }
}
            try {
                $sFile = '@www-dir@/sql/.pearweb-upgrade/' . $answers['database'] . '-' . $oldversion . '.ser';
                $curdef = unserialize(file_get_contents($sFile), ['allowed_classes' => false]);
            } catch (Exception $ex) {
                $curdef = false;
            }
}
        try {
            $uInfo = @unserialize($requser['userinfo'], ['allowed_classes' => false]);
            if ($uInfo !== false) {
                list($purpose, $moreinfo) = $uInfo;
            }
        } catch (Exception $ex) {
            $purpose = 'n/a';
            $moreinfo = 'n/a';
        }
            if (@unserialize($userinfo, ['allowed_classes' => false])) {
                $userinfo = @unserialize($userinfo, ['allowed_classes' => false]);
            $reviewed = "Reviewed: n/a";
            try {
                $uInfo = unserialize($row->reviews, ['allowed_classes' => false]);
                if ($uInfo !== false) {
                    $reviewed = "Reviewed: " . implode(", ", $uInfo);
                }
            } catch (Exception $ex) {
                // do nothing
            }
            $comment .= $reviewed;
            $reviewed = "Reviewed: n/a";
            try {
                $uInfo = unserialize($row->reviews, ['allowed_classes' => false]);
                if ($uInfo !== false) {
                    $reviewed = "Reviewed: " . implode(", ", $uInfo);
                }
            } catch (Exception $ex) {
                // do nothing
            }
            $comment .= $reviewed;