CVE-2012-5618: Weak Password Recovery Mechanism for Forgotten Password (CWE-640)
value: Ushahidi before 2.6.1 has insufficient entropy for forgot-password tokens.
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: 2020-02-04T14:15Z
Repository: https://github.com/ushahidi/Ushahidi_Web
Language: PHP
Files changed: application/controllers/login.php, application/hooks/2_settings.php, application/models/user.php
Commit date: 2012-11-12 16:41:11 +1300
Commit message: Make forgot password tokens use better random token #646

Fixes security issue discovered by Timothy D. Morgan <tim.morgan@owasp.org>
Forgotten password challenges were guessable based on users last login
and email address. Tokens are now generated based on a HMAC of login time
and email address using a salt and secret key specifically for these
tokens.

Diff: diff --git a/application/controllers/login.php b/application/controllers/login.php
index 930dc7ab..3f84c13c 100644
--- a/application/controllers/login.php
+++ b/application/controllers/login.php
@@ -318,17 +318,13 @@ class Login_Controller extends Template_Controller {
 						$riverid->requestpassword($message);
 					}
 					else
 					{
 						// Reset locally
-
-						// Secret consists of email and the last_login field.
-						// So as soon as the user logs in again,
-						// the reset link expires automatically.
-						$secret = $auth->hash_password($user->email.$user->last_login);
-						$secret_link = url::site('login/index/'.$user->id.'/'.$secret.'?reset');
-						$email_sent = $this->_email_resetlink($post->resetemail,$user->name,$secret_link);
+						$secret = $user->forgot_password_token();
+						$secret_link = url::site('login/index/'.$user->id.'/'.urlencode($secret).'?reset');
+						$email_sent = $this->_email_resetlink($post->resetemail, $user->name, $secret_link);
 					}
 
 					if ($email_sent == TRUE)
 					{
 						$message_class = 'login_success';
@@ -868,12 +864,11 @@ class Login_Controller extends Template_Controller {
 
 			}
 			else
 			{
 				// Use Standard
-
-				if($auth->hash_password($user->email.$user->last_login, $auth->find_salt($token)) == $token)
+				if($user->check_forgot_password_token($token))
 				{
 					$user->password = $password;
 					$user->save();
 				}
 				else
diff --git a/application/hooks/2_settings.php b/application/hooks/2_settings.php
index 5e91eb77..c26428e3 100644
--- a/application/hooks/2_settings.php
+++ b/application/hooks/2_settings.php
@@ -58,5 +58,14 @@ Kohana::config_set('settings.api_url_all',
 );
 
 // Additional Mime Types (KMZ/KML)
 Kohana::config_set('mimes.kml', array('text/xml'));
 Kohana::config_set('mimes.kmz', array('text/xml'));
+
+// Set 'settings.forgot_password_key' if not set already
+if ( ! Kohana::config('settings.forgot_password_secret'))
+{
+	$pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+[]{};:,.?`~';
+	$key = text::random($pool, 64);
+	Settings_Model::save_setting('forgot_password_secret', $key);
+	Kohana::config_set('settings.forgot_password_secret', $key);
+}
diff --git a/application/models/user.php b/application/models/user.php
index a4ac3726..7a746260 100644
--- a/application/models/user.php
+++ b/application/models/user.php
@@ -358,7 +358,42 @@ class User_Model extends Auth_User_Model {
 			return '';
 		
 		// Send anyone else to login
 		return 'login';
 	}
+	
+	/**
+	 * Get a new forgotten password challenge token for this user
+	 * @param string $salt Optional salt for token generation (use this)
+	 * @return string
+	 */
+	public function forgot_password_token()
+	{
+		return $this->_forgot_password_token();
+	}
+
+	/**
+	 * Check to see if forgotten password token is valid
+	 * @param string $token token to check
+	 * @return boolean is token valid
+	 **/
+	public function check_forgot_password_token($token)
+	{
+		$salt = substr($token, 0, 32);
+		return $this->_forgot_password_token($salt) == $token;
+	}
+
+	/**
+	 * Generate a forgotten password challenge token for this user
+	 * @param string $salt Optional salt for token generation (only use this for checking a token in URL)
+	 * @return string token
+	 */
+	private function _forgot_password_token($salt = FALSE)
+	{
+		// Secret consists of email and the last_login field.
+		// So as soon as the user logs in again, the reset link expires automatically.
+		$salt = $salt ? $salt : text::random('alnum', 32); // Limited charset to keep it URL friendly
+		$key = Kohana::config('settings.forgot_password_secret');
+		return $salt . hash_hmac('sha1', $this->last_login . $this->email, $salt . $key);
+	}
 
 } // End User_Model

Vulnerable code: 
						// Secret consists of email and the last_login field.
						// So as soon as the user logs in again,
						// the reset link expires automatically.
						$secret = $auth->hash_password($user->email.$user->last_login);
						$secret_link = url::site('login/index/'.$user->id.'/'.$secret.'?reset');
						$email_sent = $this->_email_resetlink($post->resetemail,$user->name,$secret_link);

				if($auth->hash_password($user->email.$user->last_login, $auth->find_salt($token)) == $token)
Fixed code: 						$secret = $user->forgot_password_token();
						$secret_link = url::site('login/index/'.$user->id.'/'.urlencode($secret).'?reset');
						$email_sent = $this->_email_resetlink($post->resetemail, $user->name, $secret_link);
				if($user->check_forgot_password_token($token))

// Set 'settings.forgot_password_key' if not set already
if ( ! Kohana::config('settings.forgot_password_secret'))
{
	$pool = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+[]{};:,.?`~';
	$key = text::random($pool, 64);
	Settings_Model::save_setting('forgot_password_secret', $key);
	Kohana::config_set('settings.forgot_password_secret', $key);
}
	
	/**
	 * Get a new forgotten password challenge token for this user
	 * @param string $salt Optional salt for token generation (use this)
	 * @return string
	 */
	public function forgot_password_token()
	{
		return $this->_forgot_password_token();
	}

	/**
	 * Check to see if forgotten password token is valid
	 * @param string $token token to check
	 * @return boolean is token valid
	 **/
	public function check_forgot_password_token($token)
	{
		$salt = substr($token, 0, 32);
		return $this->_forgot_password_token($salt) == $token;
	}

	/**
	 * Generate a forgotten password challenge token for this user
	 * @param string $salt Optional salt for token generation (only use this for checking a token in URL)
	 * @return string token
	 */
	private function _forgot_password_token($salt = FALSE)
	{
		// Secret consists of email and the last_login field.
		// So as soon as the user logs in again, the reset link expires automatically.
		$salt = $salt ? $salt : text::random('alnum', 32); // Limited charset to keep it URL friendly
		$key = Kohana::config('settings.forgot_password_secret');
		return $salt . hash_hmac('sha1', $this->last_login . $this->email, $salt . $key);
	}