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 diff --git a/db/cats_schema.sql b/db/cats_schema.sql index 37c551a95..033b654c7 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', @@ -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/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( 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 \'\'; + ', ); } 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