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