CVE-2022-2921: Exposure of Private Personal Information to an Unauthorized Actor (CWE-359)
value: Exposure of Private Personal Information to an Unauthorized Actor in GitHub repository notrinos/notrinoserp prior to v0.7. This results in privilege escalation to a system administrator account. An attacker can gain access to protected functionality such as create/update companies, install/update languages, install/activate extensions, install/activate themes and other permissive actions.
CWE description: The product does not properly prevent a person's private, personal information from being accessed by actors who either (1) are not explicitly authorized to access the information or (2) do not have the implicit consent of the person about whom the information is collected.
CVSS v3 base score: 8.8
Published: 2022-08-21T04:15Z
Repository: https://github.com/notrinos/notrinoserp
Language: PHP
Files changed: admin/change_current_user_password.php, admin/create_coy.php, admin/db/users_db.inc, admin/users.php, includes/current_user.inc
Commit date: 2022-08-21 09:07:55 +0700
Commit message: changed password hash method from md5 to bcrypt.

Diff: diff --git a/admin/change_current_user_password.php b/admin/change_current_user_password.php
index 0a25961..4515856 100644
--- a/admin/change_current_user_password.php
+++ b/admin/change_current_user_password.php
@@ -23,11 +23,11 @@ include_once($path_to_root.'/admin/db/users_db.inc');
 function can_process() {
 
 	$Auth_Result = hook_authenticate($_SESSION['wa_current_user']->username, $_POST['cur_password']);
 
 	if (!isset($Auth_Result))	// if not used external login: standard method
-		$Auth_Result = get_user_auth($_SESSION['wa_current_user']->username, md5($_POST['cur_password']));
+		$Auth_Result = authenticate_user($_SESSION['wa_current_user']->username, $_POST['cur_password']);
 
 	if (!$Auth_Result) {
 		display_error( _('Invalid password entered.'));
 		set_focus('cur_password');
 		return false;
@@ -55,11 +55,11 @@ if (isset($_POST['UPDATE_ITEM']) && check_csrf_token()) {
 
 	if (can_process()) {
 		if ($SysPrefs->allow_demo_mode)
 			display_warning(_('Password cannot be changed in demo mode.'));
 		else {
-			update_user_password($_SESSION['wa_current_user']->user, $_SESSION['wa_current_user']->username, md5($_POST['password']));
+			update_user_password($_SESSION['wa_current_user']->user, $_SESSION['wa_current_user']->username, password_hash($_POST['password'], PASSWORD_DEFAULT));
 			display_notification(_('Your password has been updated.'));
 		}
 		$Ajax->activate('_page_body');
 	}
 }
@@ -84,6 +84,6 @@ table_section_title(_('Enter your new password in the fields.'));
 
 end_table(1);
 
 submit_center( 'UPDATE_ITEM', _('Change password'), true, '',  'default');
 end_form();
-end_page();
\ No newline at end of file
+end_page();
diff --git a/admin/create_coy.php b/admin/create_coy.php
index 0ef1479..d7fb385 100644
--- a/admin/create_coy.php
+++ b/admin/create_coy.php
@@ -18,11 +18,11 @@ include_once($path_to_root.'/admin/db/company_db.inc');
 include_once($path_to_root.'/admin/db/maintenance_db.inc');
 include_once($path_to_root.'/includes/ui.inc');
 
 page(_($help_context = 'Create/Update Company'));
 
-$comp_subdirs = array('images', 'pdf_files', 'backup','js_cache', 'reporting', 'attachments');
+$comp_subdirs = array('images', 'pdf_files', 'backup', 'js_cache', 'reporting', 'attachments');
 
 simple_page_mode(true);
 /*
 	FIXME: tb_pref_counter should track prefix per database.
 */
@@ -105,11 +105,11 @@ function handle_submit($selected_id) {
 		else
 			$db_connections[$selected_id]['tbpref'] = '';
 
 		$conn = $db_connections[$selected_id];
 		if (($db = db_create_db($conn)) === false) {
-			display_error(_('Error creating Database: ') . $conn['dbname'] . _(', Please create it manually'));
+			display_error(_('Error creating Database: ').$conn['dbname']._(', Please create it manually'));
 			$error = true;
 		}
 		else {
 			if (strncmp(db_get_version(), '5.6', 3) >= 0) 
 				db_query("SET sql_mode = ''");
@@ -118,11 +118,11 @@ function handle_submit($selected_id) {
 				$error = true;
 			} 
 			else {
 				if (!isset($_POST['admpassword']) || $_POST['admpassword'] == '')
 					$_POST['admpassword'] = 'password';
-				update_admin_password($conn, md5($_POST['admpassword']));
+				update_admin_password($conn, password_hash($_POST['admpassword']), PASSWORD_DEFAULT);
 			}
 		}
 		if ($error) {
 			remove_connection($selected_id);
 			return false;
@@ -348,6 +348,6 @@ display_companies();
 display_company_edit($selected_id);
 submit_add_or_update_center($selected_id == -1, '', 'upgrade');
 
 end_form();
 
-end_page();
\ No newline at end of file
+end_page();
diff --git a/admin/db/users_db.inc b/admin/db/users_db.inc
index 4399a78..6f929fe 100644
--- a/admin/db/users_db.inc
+++ b/admin/db/users_db.inc
@@ -110,16 +110,33 @@ function delete_user($id) {
 	db_query($sql, 'could not delete user '.$id);
 }
 
 //-----------------------------------------------------------------------------------------------
 
-function get_user_auth($user_id, $password) {
+function authenticate_user($user_id, $password) {
 
-	$sql = "SELECT * FROM ".TB_PREF."users WHERE user_id = ".db_escape($user_id)." AND"
-		." password=".db_escape($password);
+	$sql1 = "SELECT password FROM ".TB_PREF."users WHERE user_id = ".db_escape($user_id);
 
-	return db_num_rows(db_query($sql, 'could not get validate user login for '.$user_id)) != 0;
+	$result = db_query($sql1, 'could not get user login for '.$user_id);
+
+	if(db_num_rows($result) == 0)
+		return false;
+
+	$hash = db_fetch($result)[0];
+
+	// The user's password hash may have been created long ago by md5 password algorithm on the old NotrinosERP versions
+	if(password_verify($password, $hash) || $hash == md5($password)) {
+		if(password_needs_rehash($hash, PASSWORD_DEFAULT) === true) {
+			$new_hash = password_hash($password, PASSWORD_DEFAULT);
+			$sql2 = "UPDATE ".TB_PREF."users SET password = ".db_escape($new_hash)." WHERE user_id = ".db_escape($user_id);
+			db_query($sql2, 'could not update password hash for '.$user_id);
+		}
+
+		return true;
+	}
+
+	return false;
 }
 
 //-----------------------------------------------------------------------------------------------
 
 function update_user_visitdate($user_id) {
diff --git a/admin/users.php b/admin/users.php
index 5fcfc2d..52fe844 100644
--- a/admin/users.php
+++ b/admin/users.php
@@ -54,16 +54,16 @@ if (($Mode=='ADD_ITEM' || $Mode=='UPDATE_ITEM') && check_csrf_token()) {
 	if (can_process($Mode == 'ADD_ITEM')) {
 		if ($selected_id != -1) {
 			update_user_prefs($selected_id, get_post(array('user_id', 'real_name', 'phone', 'email', 'role_id', 'language', 'print_profile', 'rep_popup' => 0, 'pos')));
 
 			if ($_POST['password'] != '')
-				update_user_password($selected_id, $_POST['user_id'], md5($_POST['password']));
+				update_user_password($selected_id, $_POST['user_id'], password_hash($_POST['password'], PASSWORD_DEFAULT));
 
 			display_notification_centered(_('The selected user has been updated.'));
 		} 
 		else {
-			add_user($_POST['user_id'], $_POST['real_name'], md5($_POST['password']), $_POST['phone'], $_POST['email'], $_POST['role_id'], $_POST['language'], $_POST['print_profile'], check_value('rep_popup'), $_POST['pos']);
+			add_user($_POST['user_id'], $_POST['real_name'], password_hash($_POST['password'], PASSWORD_DEFAULT), $_POST['phone'], $_POST['email'], $_POST['role_id'], $_POST['language'], $_POST['print_profile'], check_value('rep_popup'), $_POST['pos']);
 			$id = db_insert_id();
 			// use current user display preferences as start point for new user
 			$prefs = $_SESSION['wa_current_user']->prefs->get_all();
 			
 			update_user_prefs($id, array_merge($prefs, get_post(array('print_profile', 'rep_popup' => 0, 'language'))));
diff --git a/includes/current_user.inc b/includes/current_user.inc
index 6584057..b1b6e20 100644
--- a/includes/current_user.inc
+++ b/includes/current_user.inc
@@ -67,15 +67,15 @@ class current_user {
 		$lang->set_language($_SESSION['language']->code);
 		db_set_encoding($_SESSION['language']->encoding);
 
 		// Use external authentication source if any.
 		// Keep in mind you need to have user data set for $loginname
-		// in FA users table anyway to successfully log in.
+		// in NotrinosERP users table anyway to successfully log in.
 		$Auth_Result = hook_authenticate($loginname, $password);
 
 		if (!isset($Auth_Result))	// if not used: standard method
-			$Auth_Result = get_user_auth($loginname, md5($password));
+			$Auth_Result = authenticate_user($loginname, $password);
 		if ($SysPrefs->login_delay > 0)
 			write_login_filelog($loginname, $Auth_Result);
 		if ($Auth_Result) {
 			$myrow = get_user_by_login($loginname);
 			if ($myrow['language'] != $dflt_lang) {	 // refresh language and user data
@@ -149,11 +149,11 @@ class current_user {
 		$user = get_user_by_email($email);
 
 		if ($user != false) {
 
 			$password = generate_password();
-			$hash = md5($password);
+			$hash = password_hash($password);
 
 			update_user_password($user['id'], $user['user_id'], $hash);
 			
 			$sender = get_company_pref('email');
 			if (empty($sender))

Vulnerable code: 		$Auth_Result = get_user_auth($_SESSION['wa_current_user']->username, md5($_POST['cur_password']));
			update_user_password($_SESSION['wa_current_user']->user, $_SESSION['wa_current_user']->username, md5($_POST['password']));
end_page();
$comp_subdirs = array('images', 'pdf_files', 'backup','js_cache', 'reporting', 'attachments');
			display_error(_('Error creating Database: ') . $conn['dbname'] . _(', Please create it manually'));
				update_admin_password($conn, md5($_POST['admpassword']));
end_page();
function get_user_auth($user_id, $password) {
	$sql = "SELECT * FROM ".TB_PREF."users WHERE user_id = ".db_escape($user_id)." AND"
		." password=".db_escape($password);
	return db_num_rows(db_query($sql, 'could not get validate user login for '.$user_id)) != 0;
				update_user_password($selected_id, $_POST['user_id'], md5($_POST['password']));
			add_user($_POST['user_id'], $_POST['real_name'], md5($_POST['password']), $_POST['phone'], $_POST['email'], $_POST['role_id'], $_POST['language'], $_POST['print_profile'], check_value('rep_popup'), $_POST['pos']);
		// in FA users table anyway to successfully log in.
			$Auth_Result = get_user_auth($loginname, md5($password));
			$hash = md5($password);
Fixed code: 		$Auth_Result = authenticate_user($_SESSION['wa_current_user']->username, $_POST['cur_password']);
			update_user_password($_SESSION['wa_current_user']->user, $_SESSION['wa_current_user']->username, password_hash($_POST['password'], PASSWORD_DEFAULT));
end_page();
$comp_subdirs = array('images', 'pdf_files', 'backup', 'js_cache', 'reporting', 'attachments');
			display_error(_('Error creating Database: ').$conn['dbname']._(', Please create it manually'));
				update_admin_password($conn, password_hash($_POST['admpassword']), PASSWORD_DEFAULT);
end_page();
function authenticate_user($user_id, $password) {
	$sql1 = "SELECT password FROM ".TB_PREF."users WHERE user_id = ".db_escape($user_id);
	$result = db_query($sql1, 'could not get user login for '.$user_id);

	if(db_num_rows($result) == 0)
		return false;

	$hash = db_fetch($result)[0];

	// The user's password hash may have been created long ago by md5 password algorithm on the old NotrinosERP versions
	if(password_verify($password, $hash) || $hash == md5($password)) {
		if(password_needs_rehash($hash, PASSWORD_DEFAULT) === true) {
			$new_hash = password_hash($password, PASSWORD_DEFAULT);
			$sql2 = "UPDATE ".TB_PREF."users SET password = ".db_escape($new_hash)." WHERE user_id = ".db_escape($user_id);
			db_query($sql2, 'could not update password hash for '.$user_id);
		}

		return true;
	}

	return false;
				update_user_password($selected_id, $_POST['user_id'], password_hash($_POST['password'], PASSWORD_DEFAULT));
			add_user($_POST['user_id'], $_POST['real_name'], password_hash($_POST['password'], PASSWORD_DEFAULT), $_POST['phone'], $_POST['email'], $_POST['role_id'], $_POST['language'], $_POST['print_profile'], check_value('rep_popup'), $_POST['pos']);
		// in NotrinosERP users table anyway to successfully log in.
			$Auth_Result = authenticate_user($loginname, $password);
			$hash = password_hash($password);