fix: Upgraded hashing algorithm used and added requirement to confirm password for user profile changes

- Added method to upgrade all passwords to blowfish transparently
- All profile edit pages (/users/edit, /admin/users/edit, /users/change_pw) now require the user's password to be confirmed

- Thanks to cert.govt.nz for the security report.
pull/2339/head
iglocska 2017-07-12 15:38:34 +02:00
parent d377bc195b
commit 3317f56ca1
7 changed files with 247 additions and 133 deletions

View File

@ -73,6 +73,14 @@ class AppController extends Controller {
'authError' => 'Unauthorised access.',
'loginRedirect' => array('controller' => 'users', 'action' => 'routeafterlogin'),
'logoutRedirect' => array('controller' => 'users', 'action' => 'login', 'admin' => false),
'authenticate' => array(
'Form' => array(
'passwordHasher' => 'Blowfish',
'fields' => array(
'username' => 'email'
)
)
)
),
'Security',
'ACL',
@ -125,12 +133,7 @@ class AppController extends Controller {
)
);
} else {
$this->Auth->authenticate = array(
'Form' => array(
'fields' => array('username' => 'email'),
'userFields' => $auth_user_fields
)
);
$this->Auth->authenticate['Form']['userFields'] = $auth_user_fields;
}
$versionArray = $this->{$this->modelClass}->checkMISPVersion();
$this->mispVersion = implode('.', array_values($versionArray));
@ -151,7 +154,6 @@ class AppController extends Controller {
if (isset($_SERVER['HTTP_USER_AGENT'])) {
if (preg_match('/(?i)msie [2-8]/',$_SERVER['HTTP_USER_AGENT']) && !strpos($_SERVER['HTTP_USER_AGENT'], 'Opera')) throw new MethodNotAllowedException('You are using an unsecure and outdated version of IE, please download Google Chrome, Mozilla Firefox or update to a newer version of IE. If you are running IE9 or newer and still receive this error message, please make sure that you are not running your browser in compatibility mode. If you still have issues accessing the site, get in touch with your administration team at ' . Configure::read('MISP.contact'));
}
$userLoggedIn = false;
if (Configure::read('Plugin.CustomAuth_enable')) $userLoggedIn = $this->__customAuthentication($_SERVER);
if (!$userLoggedIn) {

View File

@ -84,17 +84,32 @@ class UsersController extends AppController {
throw new NotFoundException('Something went wrong. Your user account could not be accessed.');
}
if ($this->request->is('post') || $this->request->is('put')) {
// What fields should be saved (allowed to be saved)
$fieldList = array('email', 'autoalert', 'gpgkey', 'certif_public', 'nids_sid', 'contactalert', 'disabled');
if ("" != $this->request->data['User']['password'])
$fieldList[] = 'password';
// Save the data
if ($this->User->save($this->request->data, true ,$fieldList)) {
$this->Session->setFlash(__('The profile has been updated'));
$this->_refreshAuth();
$this->redirect(array('action' => 'view', $id));
} else {
$this->Session->setFlash(__('The profile could not be updated. Please, try again.'));
if (!$this->_isRest()) {
if (!empty($this->request->data['User']['current_password'])) {
$hashed = $this->User->verifyPassword($this->Auth->user('id'), $this->request->data['User']['current_password']);
if (!$hashed) {
$abortPost = true;
$this->Session->setFlash('Invalid password. Please enter your current password to continue.');
}
unset($this->request->data['User']['current_password']);
} else {
$abortPost = true;
$this->Session->setFlash('Please enter your current password to continue.');
}
}
if (!$abortPost) {
// What fields should be saved (allowed to be saved)
$fieldList = array('email', 'autoalert', 'gpgkey', 'certif_public', 'nids_sid', 'contactalert', 'disabled');
if ("" != $this->request->data['User']['password'])
$fieldList[] = 'password';
// Save the data
if ($this->User->save($this->request->data, true ,$fieldList)) {
$this->Session->setFlash(__('The profile has been updated'));
$this->_refreshAuth();
$this->redirect(array('action' => 'view', $id));
} else {
$this->Session->setFlash(__('The profile could not be updated. Please, try again.'));
}
}
} else {
$this->User->set('password', '');
@ -110,31 +125,47 @@ class UsersController extends AppController {
public function change_pw() {
$id = $this->Auth->user('id');
$this->User->id = $id;
$user = $this->User->find('first', array(
'conditions' => array('User.id' => $id),
'recursive' => -1
));
if ($this->request->is('post') || $this->request->is('put')) {
// What fields should be saved (allowed to be saved)
$fieldList[] = 'password';
// Save the data
if ($this->User->save($this->request->data, true ,$fieldList)) {
$this->Session->setFlash(__('Password Changed.'));
$this->User->saveField('email', $this->Auth->user('email'));
$this->User->saveField('change_pw', 0);
$this->_refreshAuth();
$this->redirect(array('action' => 'view', $id));
$abortPost = false;
if (!empty($this->request->data['User']['current_password'])) {
$hashed = $this->User->verifyPassword($this->Auth->user('id'), $this->request->data['User']['current_password']);
if (!$hashed) {
$abortPost = true;
$this->Session->setFlash('Invalid password. Please enter your current password to continue.');
}
unset($this->request->data['User']['current_password']);
} else {
$this->Session->setFlash(__('The password could not be updated. Please, try again.'));
$abortPost = true;
$this->Session->setFlash('Please enter your current password to continue.');
}
if (!$abortPost) {
// What fields should be saved (allowed to be saved)
$user['User']['change_pw'] = 0;
$user['User']['password'] = $this->request->data['User']['password'];
$user['User']['confirm_password'] = $this->request->data['User']['confirm_password'];
$temp = $user['User']['password'];
// Save the data
if ($this->User->save($user)) {
$this->Session->setFlash(__('Password Changed.'));
$this->_refreshAuth();
$this->__extralog("change_pw");
$this->redirect(array('action' => 'view', $id));
} else {
$this->Session->setFlash(__('The password could not be updated. Make sure you meet the minimum password length / complexity requirements.'));
}
}
} else {
$this->loadModel('Server');
$this->set('complexity', !empty(Configure::read('Security.password_policy_complexity')) ? Configure::read('Security.password_policy_complexity') : $this->Server->serverSettings['Security']['password_policy_complexity']['value']);
$this->set('length', !empty(Configure::read('Security.password_policy_length')) ? Configure::read('Security.password_policy_length') : $this->Server->serverSettings['Security']['password_policy_length']['value']);
$this->User->recursive = 0;
$this->User->read(null, $id);
$this->User->set('password', '');
$this->request->data = $this->User->data;
}
// XXX ACL roles
$this->__extralog("change_pw");
$this->loadModel('Server');
$this->set('complexity', !empty(Configure::read('Security.password_policy_complexity')) ? Configure::read('Security.password_policy_complexity') : $this->Server->serverSettings['Security']['password_policy_complexity']['value']);
$this->set('length', !empty(Configure::read('Security.password_policy_length')) ? Configure::read('Security.password_policy_length') : $this->Server->serverSettings['Security']['password_policy_length']['value']);
$this->User->recursive = 0;
$this->User->read(null, $id);
$this->User->set('password', '');
$this->request->data = $this->User->data;
$roles = $this->User->Role->find('list');
$this->set(compact('roles'));
}
@ -512,105 +543,121 @@ class UsersController extends AppController {
if (!isset($this->request->data['User'])) {
$this->request->data['User'] = $this->request->data;
}
$this->request->data['User']['id'] = $id;
if (!isset($this->request->data['User']['email'])) {
$this->request->data['User']['email'] = $userToEdit['User']['email'];
}
if (isset($this->request->data['User']['role_id']) && !array_key_exists($this->request->data['User']['role_id'], $syncRoles)) $this->request->data['User']['server_id'] = 0;
$fields = array();
$blockedFields = array('id', 'invited_by');
if (!$this->_isSiteAdmin()) {
$blockedFields[] = 'org_id';
}
foreach (array_keys($this->request->data['User']) as $field) {
if (in_array($field, $blockedFields)) {
continue;
}
if ($field != 'password') array_push($fields, $field);
}
// TODO Audit, __extralog, fields get orig
$fieldsOldValues = array();
foreach ($fields as $field) {
if ($field == 'enable_password') continue;
if ($field != 'confirm_password') array_push($fieldsOldValues, $this->User->field($field));
else array_push($fieldsOldValues, $this->User->field('password'));
}
// TODO Audit, __extralog, fields get orig END
if (
isset($this->request->data['User']['enable_password']) && $this->request->data['User']['enable_password'] != '0' &&
isset($this->request->data['User']['password']) && "" != $this->request->data['User']['password']
) {
$fields[] = 'password';
if ($this->_isRest() && !isset($this->request->data['User']['confirm_password'])) {
$this->request->data['User']['confirm_password'] = $this->request->data['User']['password'];
$fields[] = 'confirm_password';
}
}
$abortPost = false;
if (!$this->_isRest()) {
$fields[] = 'role_id';
}
if (!$this->_isSiteAdmin()) {
$this->loadModel('Role');
$this->Role->recursive = -1;
$chosenRole = $this->Role->findById($this->request->data['User']['role_id']);
if (empty($chosenRole) || (($chosenRole['Role']['id'] != $allowedRole) && ($chosenRole['Role']['perm_site_admin'] == 1 || $chosenRole['Role']['perm_regexp_access'] == 1 || $chosenRole['Role']['perm_sync'] == 1))) {
throw new Exception('You are not authorised to assign that role to a user.');
if (!empty($this->request->data['User']['current_password'])) {
$hashed = $this->User->verifyPassword($this->Auth->user('id'), $this->request->data['User']['current_password']);
if (!$hashed) {
$abortPost = true;
$this->Session->setFlash('Invalid password. Please enter your current password to continue.');
}
unset($this->request->data['User']['current_password']);
} else {
$abortPost = true;
$this->Session->setFlash('Please enter your current password to continue.');
}
}
if ($this->User->save($this->request->data, true, $fields)) {
// TODO Audit, __extralog, fields compare
// newValues to array
$fieldsNewValues = array();
if (!$abortPost) {
$this->request->data['User']['id'] = $id;
if (!isset($this->request->data['User']['email'])) {
$this->request->data['User']['email'] = $userToEdit['User']['email'];
}
if (isset($this->request->data['User']['role_id']) && !array_key_exists($this->request->data['User']['role_id'], $syncRoles)) $this->request->data['User']['server_id'] = 0;
$fields = array();
$blockedFields = array('id', 'invited_by');
if (!$this->_isSiteAdmin()) {
$blockedFields[] = 'org_id';
}
foreach (array_keys($this->request->data['User']) as $field) {
if (in_array($field, $blockedFields)) {
continue;
}
if ($field != 'password') array_push($fields, $field);
}
// TODO Audit, __extralog, fields get orig
$fieldsOldValues = array();
foreach ($fields as $field) {
if ($field != 'confirm_password') {
$newValue = $this->data['User'][$field];
if (gettype($newValue) == 'array') {
$newValueStr = '';
$cP = 0;
foreach ($newValue as $newValuePart) {
if ($cP < 2) $newValueStr .= '-' . $newValuePart;
else $newValueStr = $newValuePart . $newValueStr;
$cP++;
}
array_push($fieldsNewValues, $newValueStr);
} else {
array_push($fieldsNewValues, $newValue);
}
} else {
array_push($fieldsNewValues, $this->data['User']['password']);
if ($field == 'enable_password') continue;
if ($field != 'confirm_password') array_push($fieldsOldValues, $this->User->field($field));
else array_push($fieldsOldValues, $this->User->field('password'));
}
// TODO Audit, __extralog, fields get orig END
if (
isset($this->request->data['User']['enable_password']) && $this->request->data['User']['enable_password'] != '0' &&
isset($this->request->data['User']['password']) && "" != $this->request->data['User']['password']
) {
$fields[] = 'password';
if ($this->_isRest() && !isset($this->request->data['User']['confirm_password'])) {
$this->request->data['User']['confirm_password'] = $this->request->data['User']['password'];
$fields[] = 'confirm_password';
}
}
// compare
$fieldsResultStr = '';
$c = 0;
foreach ($fields as $field) {
if (isset($fieldsOldValues[$c]) && $fieldsOldValues[$c] != $fieldsNewValues[$c]) {
if (!$this->_isRest()) {
$fields[] = 'role_id';
}
if (!$this->_isSiteAdmin()) {
$this->loadModel('Role');
$this->Role->recursive = -1;
$chosenRole = $this->Role->findById($this->request->data['User']['role_id']);
if (empty($chosenRole) || (($chosenRole['Role']['id'] != $allowedRole) && ($chosenRole['Role']['perm_site_admin'] == 1 || $chosenRole['Role']['perm_regexp_access'] == 1 || $chosenRole['Role']['perm_sync'] == 1))) {
throw new Exception('You are not authorised to assign that role to a user.');
}
}
if ($this->User->save($this->request->data, true, $fields)) {
// TODO Audit, __extralog, fields compare
// newValues to array
$fieldsNewValues = array();
foreach ($fields as $field) {
if ($field != 'confirm_password') {
$fieldsResultStr = $fieldsResultStr . ', ' . $field . ' (' . $fieldsOldValues[$c] . ') => (' . $fieldsNewValues[$c] . ')';
$newValue = $this->data['User'][$field];
if (gettype($newValue) == 'array') {
$newValueStr = '';
$cP = 0;
foreach ($newValue as $newValuePart) {
if ($cP < 2) $newValueStr .= '-' . $newValuePart;
else $newValueStr = $newValuePart . $newValueStr;
$cP++;
}
array_push($fieldsNewValues, $newValueStr);
} else {
array_push($fieldsNewValues, $newValue);
}
} else {
array_push($fieldsNewValues, $this->data['User']['password']);
}
}
$c++;
}
$fieldsResultStr = substr($fieldsResultStr, 2);
$this->__extralog("edit", "user", $fieldsResultStr); // TODO Audit, check: modify User
// TODO Audit, __extralog, fields compare END
if ($this->_isRest()) {
$user = $this->User->find('first', array(
'conditions' => array('User.id' => $this->User->id),
'recursive' => -1
));
$user['User']['password'] = '******';
return $this->RestResponse->viewData($user, $this->response->type());
// compare
$fieldsResultStr = '';
$c = 0;
foreach ($fields as $field) {
if (isset($fieldsOldValues[$c]) && $fieldsOldValues[$c] != $fieldsNewValues[$c]) {
if ($field != 'confirm_password') {
$fieldsResultStr = $fieldsResultStr . ', ' . $field . ' (' . $fieldsOldValues[$c] . ') => (' . $fieldsNewValues[$c] . ')';
}
}
$c++;
}
$fieldsResultStr = substr($fieldsResultStr, 2);
$this->__extralog("edit", "user", $fieldsResultStr); // TODO Audit, check: modify User
// TODO Audit, __extralog, fields compare END
if ($this->_isRest()) {
$user = $this->User->find('first', array(
'conditions' => array('User.id' => $this->User->id),
'recursive' => -1
));
$user['User']['password'] = '******';
return $this->RestResponse->viewData($user, $this->response->type());
} else {
$this->Session->setFlash(__('The user has been saved'));
$this->_refreshAuth(); // in case we modify ourselves
$this->redirect(array('action' => 'index'));
}
} else {
$this->Session->setFlash(__('The user has been saved'));
$this->_refreshAuth(); // in case we modify ourselves
$this->redirect(array('action' => 'index'));
}
} else {
if ($this->_isRest()) {
return $this->RestResponse->saveFailResponse('Users', 'admin_edit', $id, $this->User->validationErrors, $this->response->type());
} else {
$this->Session->setFlash(__('The user could not be saved. Please, try again.'));
if ($this->_isRest()) {
return $this->RestResponse->saveFailResponse('Users', 'admin_edit', $id, $this->User->validationErrors, $this->response->type());
} else {
$this->Session->setFlash(__('The user could not be saved. Please, try again.'));
}
}
}
} else {
@ -702,12 +749,30 @@ class UsersController extends AppController {
throw new ForbiddenException('You have reached the maximum number of login attempts. Please wait ' . Configure::read('SecureAuth.expire') . ' seconds and try again.');
}
}
if ($this->request->is('post') || $this->request->is('put')) {
// Check the length of the user's authkey
$userPass = $this->User->find('first', array(
'conditions' => array('User.email' => $this->request->data['User']['email']),
'fields' => array('User.password'),
'recursive' => -1
));
if (strlen($userPass['User']['password']) == 40) {
$this->AdminSetting = ClassRegistry::init('AdminSetting');
$db_version = $this->AdminSetting->find('all', array('conditions' => array('setting' => 'db_version')));
$versionRequirementMet = $this->User->checkVersionRequirements($db_version[0]['AdminSetting']['value'], '2.4.77');
if ($versionRequirementMet) {
$passwordToSave = $this->request->data['User']['password'];
}
unset($this->Auth->authenticate['Form']['passwordHasher']);
}
}
if ($this->Auth->login()) {
$this->__extralog("login"); // TODO Audit, __extralog, check: customLog i.s.o. __extralog, no auth user?: $this->User->customLog('login', $this->Auth->user('id'), array('title' => '','user_id' => $this->Auth->user('id'),'email' => $this->Auth->user('email'),'org' => 'IN2'));
$this->User->Behaviors->disable('SysLogLogable.SysLogLogable');
$this->User->id = $this->Auth->user('id');
$this->User->saveField('last_login', $this->Auth->user('current_login'));
$this->User->saveField('current_login', time());
if (empty($this->Auth->authenticate['Form']['passwordHasher']) && !empty($passwordToSave)) $this->User->saveField('password', $passwordToSave);
$this->User->Behaviors->enable('SysLogLogable.SysLogLogable');
// TODO removed the auto redirect for now, due to security concerns - will look more into this
// $this->redirect($this->Auth->redirectUrl());

View File

@ -22,8 +22,8 @@
App::uses('Model', 'Model');
App::uses('LogableBehavior', 'Assets.models/behaviors');
App::uses('BlowfishPasswordHasher', 'Controller/Component/Auth');
class AppModel extends Model {
public $name;
public $loadedPubSubTool = false;
@ -47,7 +47,7 @@ class AppModel extends Model {
58 => false, 59 => false, 60 => false, 61 => false, 62 => false,
63 => false, 64 => false, 65 => false, 66 => false, 67 => true,
68 => false, 69 => false, 71 => false, 72 => false, 73 => false,
75 => false
75 => false, 77 => false
)
)
);
@ -699,6 +699,9 @@ class AppModel extends Model {
$this->__addIndex('attributes', 'value1', 255);
$this->__addIndex('attributes', 'value2', 255);
break;
case '2.4.77':
$sqlArray[] = 'ALTER TABLE `users` CHANGE `password` `password` VARCHAR(255) COLLATE utf8_bin NOT NULL;';
break;
case 'fixNonEmptySharingGroupID':
$sqlArray[] = 'UPDATE `events` SET `sharing_group_id` = 0 WHERE `distribution` != 4;';
$sqlArray[] = 'UPDATE `attributes` SET `sharing_group_id` = 0 WHERE `distribution` != 4;';
@ -1051,4 +1054,10 @@ class AppModel extends Model {
$this->loadedPubSubTool = $pubSubTool;
return true;
}
public function checkVersionRequirements($versionString, $minVersion) {
$version = explode('.', $versionString);
$minVersion = explode('.', $minVersion);
return ($version[0] >= $minVersion[0] && $version[1] >= $minVersion[1] && $version[2] >= $minVersion[2]);
}
}

View File

@ -251,7 +251,8 @@ class User extends AppModel {
public function beforeSave($options = array()) {
$this->data[$this->alias]['date_modified'] = time();
if (isset($this->data[$this->alias]['password'])) {
$this->data[$this->alias]['password'] = AuthComponent::password($this->data[$this->alias]['password']);
$passwordHasher = new BlowfishPasswordHasher();
$this->data[$this->alias]['password'] = $passwordHasher->hash($this->data[$this->alias]['password']);
}
return true;
}
@ -1026,4 +1027,21 @@ class User extends AppModel {
)
));
}
public function verifyPassword($user_id, $password) {
$currentUser = $this->find('first', array(
'conditions' => array('User.id' => $user_id),
'recursive' => -1,
'fields' => array('User.password')
));
if (empty($currentUser)) return false;
if (strlen($currentUser['User']['password']) == 40) {
App::uses('SimplePasswordHasher', 'Controller/Component/Auth');
$passwordHasher = new SimplePasswordHasher();
} else {
$passwordHasher = new BlowfishPasswordHasher();
}
$hashed = $passwordHasher->check($password, $currentUser['User']['password']);
return $hashed;
}
}

View File

@ -79,6 +79,12 @@
?>
</fieldset>
<div style="border-bottom: 1px solid #e5e5e5;width:100%;">&nbsp;</div>
<div class="clear" style="margin-top:10px;">
<?php
echo $this->Form->input('current_password', array('type' => 'password', 'div' => false, 'class' => 'input password required', 'label' => 'Confirm with your current password'));
?>
</div>
<?php
echo $this->Form->button(__('Submit'), array('class' => 'btn btn-primary'));
echo $this->Form->end();?>

View File

@ -11,6 +11,12 @@
echo $this->Form->input('confirm_password', array('type' => 'password', 'div' => array('class' => 'input password required')));
?>
</fieldset>
<div style="border-bottom: 1px solid #e5e5e5;width:100%;">&nbsp;</div>
<div class="clear" style="margin-top:10px;">
<?php
echo $this->Form->input('current_password', array('type' => 'password', 'div' => false, 'class' => 'input password required', 'label' => 'Confirm with your current password'));
?>
</div>
<?php
echo $this->Form->button(__('Submit'), array('class' => 'btn btn-primary'));
echo $this->Form->end();

View File

@ -29,8 +29,16 @@
echo $this->Form->input('contactalert', array('label' => 'Receive alerts from "contact reporter" requests', 'type' => 'checkbox'));
?>
</fieldset>
<?php echo $this->Form->button(__('Submit'), array('class' => 'btn btn-primary'));
echo $this->Form->end();?>
<div style="border-bottom: 1px solid #e5e5e5;width:100%;">&nbsp;</div>
<div class="clear" style="margin-top:10px;">
<?php
echo $this->Form->input('current_password', array('type' => 'password', 'div' => false, 'class' => 'input password required', 'label' => 'Confirm with your current password'));
?>
</div>
<?php
echo $this->Form->button(__('Submit'), array('class' => 'btn btn-primary'));
echo $this->Form->end();
?>
</div>
<?php
$user['User']['id'] = $id;