Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Security.MD
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 3 additions & 3 deletions db/cats_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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',
Expand Down Expand Up @@ -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` */
Expand Down
2 changes: 1 addition & 1 deletion installwizard.php
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@
<br />
You may now login to OpenCATS. If it is a new installation, use the following logon information:<br /><br />
Username: admin<br />
Password: admin<br />
Password: cats<br />
<br />
<br />
OpenCATS will periodically check for new versions of the software from catsone.com, and will send non confidential information about your
Expand Down
110 changes: 101 additions & 9 deletions lib/Users.php
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down
4 changes: 4 additions & 0 deletions modules/install/Schema.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 \'\';
',

);
}
Expand Down
2 changes: 1 addition & 1 deletion modules/install/ajax/ui.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down