From 600d1ad7fa3ad2856b298ad0845a92883f135b43 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Sun, 14 Dec 2025 10:59:43 +0000 Subject: [PATCH 1/4] Move password handling to password_hash() with legacy MD5 support --- lib/Users.php | 110 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 101 insertions(+), 9 deletions(-) mode change 100755 => 100644 lib/Users.php diff --git a/lib/Users.php b/lib/Users.php old mode 100755 new mode 100644 index e4b75e615..5420b02c3 --- a/lib/Users.php +++ b/lib/Users.php @@ -90,7 +90,7 @@ public function add($lastName, $firstName, $email, $username, $password, $accessLevel, $eeoIsVisible = false, $userSiteID = -1) { - $md5pwd = $password == LDAPUSER_PASSWORD ? $password : md5($password); + $hashedPassword = $password == LDAPUSER_PASSWORD ? $password : $this->hashPassword($password); $userSiteID = $userSiteID < 0 ? $this->_siteID : $userSiteID; $sql = sprintf( "INSERT INTO user ( @@ -118,7 +118,7 @@ public function add($lastName, $firstName, $email, $username, $password, %s )", $this->_db->makeQueryString($username), - $this->_db->makeQueryString($md5pwd), + $this->_db->makeQueryString($hashedPassword), $this->_db->makeQueryInteger($accessLevel), $this->_db->makeQueryString($email), $this->_db->makeQueryString($firstName), @@ -676,7 +676,19 @@ public function changePassword($userID, $currentPassword, $newPassword) } /* Is the user's supplied password correct? */ - if ($rs['password'] !== md5($currentPassword)) + /* FIXME: This code relies on verifyAndMigratePassword() to handle both legacy + * MD5 hashes and modern password_hash() output. If verifyAndMigratePassword() + * is changed in a future release (for example when removing MD5 support or + * switching to a different algorithm), this password check must be reviewed + * and adjusted accordingly. + * + * Previous MD5-only implementation for reference: + * if ($rs['password'] !== md5($currentPassword)) + * { + * return LOGIN_INVALID_PASSWORD; + * } + */ + if (!$this->verifyAndMigratePassword($userID, $currentPassword, $rs['password'])) { return LOGIN_INVALID_PASSWORD; } @@ -694,14 +706,15 @@ public function changePassword($userID, $currentPassword, $newPassword) } /* Change the user's password. */ + $newPasswordHash = $this->hashPassword($newPassword); $sql = sprintf( "UPDATE user SET - password = md5(%s) + password = %s WHERE user.user_id = %s", - $this->_db->makeQueryString($newPassword), + $this->_db->makeQueryString($newPasswordHash), $this->_db->makeQueryInteger($userID) ); $this->_db->query($sql); @@ -748,14 +761,15 @@ public function resetPassword($userID, $newPassword) } /* Change the user's password. */ + $newPasswordHash = $this->hashPassword($newPassword); $sql = sprintf( "UPDATE user SET - password = md5(%s) + password = %s WHERE user.user_id = %s", - $this->_db->makeQueryString($newPassword), + $this->_db->makeQueryString($newPasswordHash), $this->_db->makeQueryInteger($userID) ); $this->_db->query($sql); @@ -798,7 +812,8 @@ public function isCorrectLogin($username, $password) "SELECT user.user_name AS username, user.password AS password, - user.access_level AS accessLevel + user.access_level AS accessLevel, + user.user_id AS userID FROM user WHERE @@ -837,10 +852,12 @@ public function isCorrectLogin($username, $password) return LOGIN_INVALID_USER; } else { /* Is the user's supplied password correct? */ - if ($rs['password'] !== md5($password)) + if (!$this->verifyAndMigratePassword((int) $rs['userID'], $password, $rs['password'])) { return LOGIN_INVALID_PASSWORD; } + + $this->rehashPasswordIfNeeded((int) $rs['userID'], $rs['password'], $password); } if (!$existsInDB && $existsInLDAP) { @@ -1236,6 +1253,81 @@ public function getAutomatedUser() return $rs; } + private function hashPassword($password) + { + return password_hash($password, PASSWORD_DEFAULT); + } + + /** + * Upgrades an existing password hash to the current PASSWORD_DEFAULT algorithm/options + * after a successful login, if password_needs_rehash() indicates that an update + * is required. + * + * @param int $userID ID of the user whose password hash may be updated. + * @param string $storedHash Hash value as it was read from the database before verification. + * @param string $password Plain-text password that was just successfully verified. + * + * @return void + */ + private function rehashPasswordIfNeeded($userID, $storedHash, $password) + { + if ($this->isLegacyPasswordHash($storedHash) || $storedHash === LDAPUSER_PASSWORD) + { + return; + } + + if (password_needs_rehash($storedHash, PASSWORD_DEFAULT)) + { + $this->updatePasswordHash($userID, $password); + } + } + + /* FIXME: The following methods exist only for backwards compatibility with legacy MD5 password hashes. + * They also perform a one-time lazy migration from MD5 to password_hash(). + * After several releases with the new algorithm in place, this MD5-related code should be removed + * from the codebase. + */ + + private function isLegacyPasswordHash($hash) + { + return (bool) preg_match('/^[0-9a-f]{32}$/i', $hash); + } + + private function verifyAndMigratePassword($userID, $password, $storedHash) + { + if ($this->isLegacyPasswordHash($storedHash)) + { + if (md5($password) !== $storedHash) + { + return false; + } + + $this->updatePasswordHash($userID, $password); + + return true; + } + + return password_verify($password, $storedHash); + } + + private function updatePasswordHash($userID, $password) + { + $sql = sprintf( + "UPDATE + user + SET + password = %s + WHERE + user.user_id = %s", + $this->_db->makeQueryString($this->hashPassword($password)), + $this->_db->makeQueryInteger($userID) + ); + + $this->_db->query($sql); + } + + // End of temporary MD5 compatibility and lazy migration code. + public function isUserLDAP($userID) { $sql = sprintf( From 800fa06381554d007de917790e6fd4185276e171 Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Sun, 14 Dec 2025 12:14:13 +0100 Subject: [PATCH 2/4] Update Security.md for password_hash() support --- Security.MD | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Security.MD b/Security.MD index 9eab4bf3a..35b89b446 100644 --- a/Security.MD +++ b/Security.MD @@ -8,7 +8,7 @@ In order to give the community time to respond and upgrade we strongly urge you ### Password Storage -OpenCATS uses MD5 hashing to store passwords. This will be replaced in future versions +OpenCATS hashes user passwords using PHP’s password_hash() and verifies them with password_verify(), using PASSWORD_DEFAULT (the default algorithm selected by the running PHP version). ### XSS From f151d2def872a2a8ac73f1f519d4b482ede67fdc Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Sun, 14 Dec 2025 12:35:20 +0100 Subject: [PATCH 3/4] Increase user.password column length to VARCHAR(255) --- db/cats_schema.sql | 4 ++-- modules/install/Schema.php | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/db/cats_schema.sql b/db/cats_schema.sql index 37c551a95..ebd9f85d2 100755 --- a/db/cats_schema.sql +++ b/db/cats_schema.sql @@ -859,7 +859,7 @@ insert into `module_schema`(`module_schema_id`,`name`,`version`) values (9,'ext insert into `module_schema`(`module_schema_id`,`name`,`version`) values (10,'graphs',0); insert into `module_schema`(`module_schema_id`,`name`,`version`) values (11,'home',0); insert into `module_schema`(`module_schema_id`,`name`,`version`) values (12,'import',0); -insert into `module_schema`(`module_schema_id`,`name`,`version`) values (13,'install',363); +insert into `module_schema`(`module_schema_id`,`name`,`version`) values (13,'install',365); insert into `module_schema`(`module_schema_id`,`name`,`version`) values (14,'joborders',0); insert into `module_schema`(`module_schema_id`,`name`,`version`) values (15,'lists',0); insert into `module_schema`(`module_schema_id`,`name`,`version`) values (16,'login',0); @@ -1072,7 +1072,7 @@ CREATE TABLE `user` ( `site_id` int(11) NOT NULL DEFAULT '0', `user_name` varchar(64) COLLATE utf8_unicode_ci NOT NULL DEFAULT '', `email` varchar(128) COLLATE utf8_unicode_ci DEFAULT NULL, - `password` varchar(128) COLLATE utf8_unicode_ci NOT NULL DEFAULT '', + `password` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '', `access_level` int(11) NOT NULL DEFAULT '100', `can_change_password` int(1) NOT NULL DEFAULT '1', `is_test_user` int(1) NOT NULL DEFAULT '0', diff --git a/modules/install/Schema.php b/modules/install/Schema.php index 495405504..6a98eb076 100755 --- a/modules/install/Schema.php +++ b/modules/install/Schema.php @@ -1328,6 +1328,10 @@ public static function get() '364' => ' UPDATE user SET password = md5(password) WHERE can_change_password=1; ', + '365' => ' + ALTER TABLE `user` + MODIFY `password` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT \'\'; + ', ); } From d2d7bfd093032190b9297afbd638cb6738136afa Mon Sep 17 00:00:00 2001 From: anonymoususer72041 <247563575+anonymoususer72041@users.noreply.github.com> Date: Sat, 27 Dec 2025 13:19:42 +0100 Subject: [PATCH 4/4] fix: make default admin login work on fresh installations --- db/cats_schema.sql | 2 +- installwizard.php | 2 +- modules/install/ajax/ui.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/db/cats_schema.sql b/db/cats_schema.sql index ebd9f85d2..033b654c7 100755 --- a/db/cats_schema.sql +++ b/db/cats_schema.sql @@ -1105,7 +1105,7 @@ CREATE TABLE `user` ( /*Data for the table `user` */ -insert into `user`(`user_id`,`site_id`,`user_name`,`email`,`password`,`access_level`,`can_change_password`,`is_test_user`,`last_name`,`first_name`,`is_demo`,`categories`,`session_cookie`,`pipeline_entries_per_page`,`column_preferences`,`force_logout`,`title`,`phone_work`,`phone_cell`,`phone_other`,`address`,`notes`,`company`,`city`,`state`,`zip_code`,`country`,`can_see_eeo_info`) values (1,1,'admin','admin@testdomain.com','admin',500,1,0,'Administrator','CATS',0,NULL,'CATS=e29233aabf2cdb71373582023ff9747e',15,'a:4:{s:31:\"home:ImportantPipelineDashboard\";a:6:{i:0;a:2:{s:4:\"name\";s:10:\"First Name\";s:5:\"width\";i:85;}i:1;a:2:{s:4:\"name\";s:9:\"Last Name\";s:5:\"width\";i:75;}i:2;a:2:{s:4:\"name\";s:6:\"Status\";s:5:\"width\";i:75;}i:3;a:2:{s:4:\"name\";s:8:\"Position\";s:5:\"width\";i:275;}i:4;a:2:{s:4:\"name\";s:7:\"Company\";s:5:\"width\";i:210;}i:5;a:2:{s:4:\"name\";s:8:\"Modified\";s:5:\"width\";i:80;}}s:18:\"home:CallsDataGrid\";a:2:{i:0;a:2:{s:4:\"name\";s:4:\"Time\";s:5:\"width\";i:90;}i:1;a:2:{s:4:\"name\";s:4:\"Name\";s:5:\"width\";i:175;}}s:39:\"candidates:candidatesListByViewDataGrid\";a:9:{i:0;a:2:{s:4:\"name\";s:11:\"Attachments\";s:5:\"width\";i:31;}i:1;a:2:{s:4:\"name\";s:10:\"First Name\";s:5:\"width\";i:75;}i:2;a:2:{s:4:\"name\";s:9:\"Last Name\";s:5:\"width\";i:85;}i:3;a:2:{s:4:\"name\";s:4:\"City\";s:5:\"width\";i:75;}i:4;a:2:{s:4:\"name\";s:5:\"State\";s:5:\"width\";i:50;}i:5;a:2:{s:4:\"name\";s:10:\"Key Skills\";s:5:\"width\";i:215;}i:6;a:2:{s:4:\"name\";s:5:\"Owner\";s:5:\"width\";i:65;}i:7;a:2:{s:4:\"name\";s:7:\"Created\";s:5:\"width\";i:60;}i:8;a:2:{s:4:\"name\";s:8:\"Modified\";s:5:\"width\";i:60;}}s:25:\"activity:ActivityDataGrid\";a:7:{i:0;a:2:{s:4:\"name\";s:4:\"Date\";s:5:\"width\";i:110;}i:1;a:2:{s:4:\"name\";s:10:\"First Name\";s:5:\"width\";i:85;}i:2;a:2:{s:4:\"name\";s:9:\"Last Name\";s:5:\"width\";i:75;}i:3;a:2:{s:4:\"name\";s:9:\"Regarding\";s:5:\"width\";i:125;}i:4;a:2:{s:4:\"name\";s:8:\"Activity\";s:5:\"width\";i:65;}i:5;a:2:{s:4:\"name\";s:5:\"Notes\";s:5:\"width\";i:240;}i:6;a:2:{s:4:\"name\";s:10:\"Entered By\";s:5:\"width\";i:60;}}}',0,'','','','',NULL,NULL,NULL,NULL,NULL,NULL,NULL,0); +insert into `user`(`user_id`,`site_id`,`user_name`,`email`,`password`,`access_level`,`can_change_password`,`is_test_user`,`last_name`,`first_name`,`is_demo`,`categories`,`session_cookie`,`pipeline_entries_per_page`,`column_preferences`,`force_logout`,`title`,`phone_work`,`phone_cell`,`phone_other`,`address`,`notes`,`company`,`city`,`state`,`zip_code`,`country`,`can_see_eeo_info`) values (1,1,'admin','admin@testdomain.com',md5('cats'),500,1,0,'Administrator','CATS',0,NULL,'CATS=e29233aabf2cdb71373582023ff9747e',15,'a:4:{s:31:\"home:ImportantPipelineDashboard\";a:6:{i:0;a:2:{s:4:\"name\";s:10:\"First Name\";s:5:\"width\";i:85;}i:1;a:2:{s:4:\"name\";s:9:\"Last Name\";s:5:\"width\";i:75;}i:2;a:2:{s:4:\"name\";s:6:\"Status\";s:5:\"width\";i:75;}i:3;a:2:{s:4:\"name\";s:8:\"Position\";s:5:\"width\";i:275;}i:4;a:2:{s:4:\"name\";s:7:\"Company\";s:5:\"width\";i:210;}i:5;a:2:{s:4:\"name\";s:8:\"Modified\";s:5:\"width\";i:80;}}s:18:\"home:CallsDataGrid\";a:2:{i:0;a:2:{s:4:\"name\";s:4:\"Time\";s:5:\"width\";i:90;}i:1;a:2:{s:4:\"name\";s:4:\"Name\";s:5:\"width\";i:175;}}s:39:\"candidates:candidatesListByViewDataGrid\";a:9:{i:0;a:2:{s:4:\"name\";s:11:\"Attachments\";s:5:\"width\";i:31;}i:1;a:2:{s:4:\"name\";s:10:\"First Name\";s:5:\"width\";i:75;}i:2;a:2:{s:4:\"name\";s:9:\"Last Name\";s:5:\"width\";i:85;}i:3;a:2:{s:4:\"name\";s:4:\"City\";s:5:\"width\";i:75;}i:4;a:2:{s:4:\"name\";s:5:\"State\";s:5:\"width\";i:50;}i:5;a:2:{s:4:\"name\";s:10:\"Key Skills\";s:5:\"width\";i:215;}i:6;a:2:{s:4:\"name\";s:5:\"Owner\";s:5:\"width\";i:65;}i:7;a:2:{s:4:\"name\";s:7:\"Created\";s:5:\"width\";i:60;}i:8;a:2:{s:4:\"name\";s:8:\"Modified\";s:5:\"width\";i:60;}}s:25:\"activity:ActivityDataGrid\";a:7:{i:0;a:2:{s:4:\"name\";s:4:\"Date\";s:5:\"width\";i:110;}i:1;a:2:{s:4:\"name\";s:10:\"First Name\";s:5:\"width\";i:85;}i:2;a:2:{s:4:\"name\";s:9:\"Last Name\";s:5:\"width\";i:75;}i:3;a:2:{s:4:\"name\";s:9:\"Regarding\";s:5:\"width\";i:125;}i:4;a:2:{s:4:\"name\";s:8:\"Activity\";s:5:\"width\";i:65;}i:5;a:2:{s:4:\"name\";s:5:\"Notes\";s:5:\"width\";i:240;}i:6;a:2:{s:4:\"name\";s:10:\"Entered By\";s:5:\"width\";i:60;}}}',0,'','','','',NULL,NULL,NULL,NULL,NULL,NULL,NULL,0); insert into `user`(`user_id`,`site_id`,`user_name`,`email`,`password`,`access_level`,`can_change_password`,`is_test_user`,`last_name`,`first_name`,`is_demo`,`categories`,`session_cookie`,`pipeline_entries_per_page`,`column_preferences`,`force_logout`,`title`,`phone_work`,`phone_cell`,`phone_other`,`address`,`notes`,`company`,`city`,`state`,`zip_code`,`country`,`can_see_eeo_info`) values (1250,180,'cats@rootadmin','0','cantlogin',0,0,0,'Automated','CATS',0,NULL,NULL,15,NULL,0,'','','','',NULL,NULL,NULL,NULL,NULL,NULL,NULL,0); /*Table structure for table `user_login` */ diff --git a/installwizard.php b/installwizard.php index 911d5fd39..ba56d4ab6 100755 --- a/installwizard.php +++ b/installwizard.php @@ -416,7 +416,7 @@
You may now login to OpenCATS. If it is a new installation, use the following logon information:

Username: admin
- Password: admin
+ Password: cats


OpenCATS will periodically check for new versions of the software from catsone.com, and will send non confidential information about your diff --git a/modules/install/ajax/ui.php b/modules/install/ajax/ui.php index 53db04e79..b512f5af5 100755 --- a/modules/install/ajax/ui.php +++ b/modules/install/ajax/ui.php @@ -1045,7 +1045,7 @@ MySQLConnect(); /* Determine if a default user is set. */ - $rs = MySQLQuery("SELECT * FROM user WHERE user_name = 'admin' AND password = 'cats'"); + $rs = MySQLQuery("SELECT * FROM user WHERE user_name = 'admin' AND password = md5('cats')"); if ($rs && mysqli_fetch_row($rs)) { //Default user set