Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement password salts #7

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
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
8 changes: 4 additions & 4 deletions UserModule.php
Original file line number Diff line number Diff line change
Expand Up @@ -160,14 +160,14 @@ public static function t($str='',$params=array(),$dic='user') {
/**
* @return hash string.
*/
public static function encrypting($string="") {
public static function encrypting($string="", $salt="") {
$hash = Yii::app()->getModule('user')->hash;
if ($hash=="md5")
return md5($string);
return md5($string.$salt);
if ($hash=="sha1")
return sha1($string);
return sha1($string.$salt);
else
return hash($hash,$string);
return hash($hash,$string.$salt);
}

/**
Expand Down
14 changes: 12 additions & 2 deletions components/UserIdentity.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,16 @@ public function authenticate()
} else {
$user=User::model()->notsafe()->findByAttributes(array('username'=>$this->username));
}
if($user===null)
if($user===null) {
if (strpos($this->username,"@")) {
$this->errorCode=self::ERROR_EMAIL_INVALID;
} else {
$this->errorCode=self::ERROR_USERNAME_INVALID;
}
else if(Yii::app()->getModule('user')->encrypting($this->password)!==$user->password)
return false;
}

if(Yii::app()->getModule('user')->encrypting($this->password, $user->salt)!==$user->password)
$this->errorCode=self::ERROR_PASSWORD_INVALID;
else if($user->status==0&&Yii::app()->getModule('user')->loginNotActiv==false)
$this->errorCode=self::ERROR_STATUS_NOTACTIV;
Expand All @@ -42,6 +45,13 @@ public function authenticate()
$this->_id=$user->id;
$this->username=$user->username;
$this->errorCode=self::ERROR_NONE;

//when user has no salt, let's be generous and give him some.
if(empty($user->salt)) {
$user->salt = User::getNewSalt();
$user->password = Yii::app()->getModule('user')->encrypting($this->password, $user->salt);
$user->save();
}
}
return !$this->errorCode;
}
Expand Down
15 changes: 11 additions & 4 deletions controllers/AdminController.php
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ public function actionAdmin()
public function actionView()
{
$model = $this->loadModel();
$model->password = '';
$this->render('view',array(
'model'=>$model,
));
Expand All @@ -85,7 +86,8 @@ public function actionCreate()
$profile->attributes=$_POST['Profile'];
$profile->user_id=0;
if($model->validate()&&$profile->validate()) {
$model->password=Yii::app()->controller->module->encrypting($model->password);
$model->salt = User::getNewSalt();
$model->password=Yii::app()->controller->module->encrypting($model->password,$model->salt);
if($model->save()) {
$profile->user_id=$model->id;
$profile->save();
Expand Down Expand Up @@ -116,16 +118,21 @@ public function actionUpdate()

if($model->validate()&&$profile->validate()) {
$old_password = User::model()->notsafe()->findByPk($model->id);
if ($old_password->password!=$model->password) {
$model->password=Yii::app()->controller->module->encrypting($model->password);
$old_salt = $old_password->salt;
$old_password = $old_password->password;
if (isset($_POST['User']['password']) && $old_password != Yii::app()->controller->module->encrypting($model->password,$old_salt)) {
$model->salt = User::getNewSalt();
$model->password=Yii::app()->controller->module->encrypting($model->password,$model->salt);
$model->activkey=Yii::app()->controller->module->encrypting(microtime().$model->password);
} else {
unset($model->password);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that this unset() is somewhat important, because it makes sure this property is not changed in the database.

When an admin decides to change a users password, but does so by entering the old password (above statement evaluates to false), this property would hold that old password in plaintext form and the save() below would commit that to the database, causing a major security risk (exposing the admin's password) and effectively rendering the account useless because it would become impossible to login again with those credentials.

Ofcourse chances of that happening to another users account are small, but it may very well happen when updating his own account.

}
$model->save();
$profile->save();
$this->redirect(array('view','id'=>$model->id));
} else $profile->validate();
}

$model->password = '';
$this->render('update',array(
'model'=>$model,
'profile'=>$profile,
Expand Down
6 changes: 3 additions & 3 deletions controllers/LoginController.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ public function actionLogin()
$model->attributes=$_POST['UserLogin'];
// validate user input and redirect to previous page if valid
if($model->validate()) {
$this->lastViset();
if (Yii::app()->getBaseUrl()."/index.php" === Yii::app()->user->returnUrl)
$this->lastVisit();
if (Yii::app()->user->returnUrl=='/index.php')
$this->redirect(Yii::app()->controller->module->returnUrl);
else
$this->redirect(Yii::app()->user->returnUrl);
Expand All @@ -30,7 +30,7 @@ public function actionLogin()
$this->redirect(Yii::app()->controller->module->returnUrl);
}

private function lastViset() {
private function lastVisit() {
$lastVisit = User::model()->notsafe()->findByPk(Yii::app()->user->id);
$lastVisit->lastvisit_at = date('Y-m-d H:i:s');
$lastVisit->save();
Expand Down
5 changes: 3 additions & 2 deletions controllers/ProfileController.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,9 @@ public function actionChangepassword() {
$model->attributes=$_POST['UserChangePassword'];
if($model->validate()) {
$new_password = User::model()->notsafe()->findbyPk(Yii::app()->user->id);
$new_password->password = UserModule::encrypting($model->password);
$new_password->activkey=UserModule::encrypting(microtime().$model->password);
$new_password->salt = User::getNewSalt();
$new_password->password = UserModule::encrypting($model->password,$new_password->salt);
$new_password->activkey = UserModule::encrypting(microtime().$model->password);
$new_password->save();
Yii::app()->user->setFlash('profileMessage',UserModule::t("New password is saved."));
$this->redirect(array("profile"));
Expand Down
78 changes: 41 additions & 37 deletions controllers/RegistrationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,42 +39,46 @@ public function actionRegistration() {
$profile->attributes=((isset($_POST['Profile'])?$_POST['Profile']:array()));
if($model->validate()&&$profile->validate())
{
$soucePassword = $model->password;
$model->activkey=UserModule::encrypting(microtime().$model->password);
$model->password=UserModule::encrypting($model->password);
$model->verifyPassword=UserModule::encrypting($model->verifyPassword);
$model->superuser=0;
$model->status=((Yii::app()->controller->module->activeAfterRegister)?User::STATUS_ACTIVE:User::STATUS_NOACTIVE);

if ($model->save()) {
$profile->user_id=$model->id;
$profile->save();
if (Yii::app()->controller->module->sendActivationMail) {
$activation_url = $this->createAbsoluteUrl('/user/activation/activation',array("activkey" => $model->activkey, "email" => $model->email));
UserModule::sendMail($model->email,UserModule::t("You registered from {site_name}",array('{site_name}'=>Yii::app()->name)),UserModule::t("Please activate you account go to {activation_url}",array('{activation_url}'=>$activation_url)));
}

if ((Yii::app()->controller->module->loginNotActiv||(Yii::app()->controller->module->activeAfterRegister&&Yii::app()->controller->module->sendActivationMail==false))&&Yii::app()->controller->module->autoLogin) {
$identity=new UserIdentity($model->username,$soucePassword);
$identity->authenticate();
Yii::app()->user->login($identity,0);
$this->redirect(Yii::app()->controller->module->returnUrl);
} else {
if (!Yii::app()->controller->module->activeAfterRegister&&!Yii::app()->controller->module->sendActivationMail) {
Yii::app()->user->setFlash('registration',UserModule::t("Thank you for your registration. Contact Admin to activate your account."));
} elseif(Yii::app()->controller->module->activeAfterRegister&&Yii::app()->controller->module->sendActivationMail==false) {
Yii::app()->user->setFlash('registration',UserModule::t("Thank you for your registration. Please {{login}}.",array('{{login}}'=>CHtml::link(UserModule::t('Login'),Yii::app()->controller->module->loginUrl))));
} elseif(Yii::app()->controller->module->loginNotActiv) {
Yii::app()->user->setFlash('registration',UserModule::t("Thank you for your registration. Please check your email or login."));
} else {
Yii::app()->user->setFlash('registration',UserModule::t("Thank you for your registration. Please check your email."));
}
$this->refresh();
}
}
} else $profile->validate();
}
$this->render('/user/registration',array('model'=>$model,'profile'=>$profile));
}
$model->salt = User::getNewSalt();

$sourcePassword = $model->password;
$model->activkey=UserModule::encrypting(microtime().$model->password);
$model->password=UserModule::encrypting($model->password, $model->salt);
$model->verifyPassword=UserModule::encrypting($model->verifyPassword, $model->salt);
$model->superuser=0;
$model->status=((Yii::app()->controller->module->activeAfterRegister)?User::STATUS_ACTIVE:User::STATUS_NOACTIVE);

if ($model->save()) {
$profile->user_id=$model->id;
$profile->save();
if (Yii::app()->controller->module->sendActivationMail) {
$activation_url = $this->createAbsoluteUrl('/user/activation/activation',array("activkey" => $model->activkey, "email" => $model->email));
UserModule::sendMail($model->email,UserModule::t("You registered from {site_name}",array('{site_name}'=>Yii::app()->name)),UserModule::t("Please activate you account go to {activation_url}",array('{activation_url}'=>$activation_url)));
}

if ((Yii::app()->controller->module->loginNotActiv||(Yii::app()->controller->module->activeAfterRegister&&Yii::app()->controller->module->sendActivationMail==false))&&Yii::app()->controller->module->autoLogin) {
$identity=new UserIdentity($model->username,$sourcePassword);
$identity->authenticate();
Yii::app()->user->login($identity,0);
$this->redirect(Yii::app()->controller->module->returnUrl);
} else {
if (!Yii::app()->controller->module->activeAfterRegister&&!Yii::app()->controller->module->sendActivationMail) {
Yii::app()->user->setFlash('registration',UserModule::t("Thank you for your registration. Contact Admin to activate your account."));
} elseif(Yii::app()->controller->module->activeAfterRegister&&Yii::app()->controller->module->sendActivationMail==false) {
Yii::app()->user->setFlash('registration',UserModule::t("Thank you for your registration. Please {{login}}.",array('{{login}}'=>CHtml::link(UserModule::t('Login'),Yii::app()->controller->module->loginUrl))));
} elseif(Yii::app()->controller->module->loginNotActiv) {
Yii::app()->user->setFlash('registration',UserModule::t("Thank you for your registration. Please check your email or login."));
} else {
Yii::app()->user->setFlash('registration',UserModule::t("Thank you for your registration. Please check your email."));
}
$this->refresh();
}
}
} else $profile->validate();
}
$model->password = '';
$model->verifyPassword = '';
$this->render('/user/registration',array('model'=>$model,'profile'=>$profile));
}
}
}
1 change: 1 addition & 0 deletions data/schema.mysql.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ CREATE TABLE `tbl_users` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(20) NOT NULL,
`password` varchar(128) NOT NULL,
`salt` varchar(40) NULL DEFAULT NULL,
`email` varchar(128) NOT NULL,
`activkey` varchar(128) NOT NULL DEFAULT '',
`create_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
Expand Down
3 changes: 2 additions & 1 deletion data/schema.sqlite.sql
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ CREATE TABLE tbl_users (
create_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
lastvisit_at TIMESTAMP NOT NULL DEFAULT '0000-00-00 00:00:00',
superuser int(1) NOT NULL DEFAULT '0',
status int(1) NOT NULL DEFAULT '0'
status int(1) NOT NULL DEFAULT '0',
salt varchar(40) DEFAULT NULL
);

CREATE TABLE tbl_profiles (
Expand Down
Binary file modified data/user.sqlite
Binary file not shown.
64 changes: 64 additions & 0 deletions migrations/m121001_001625_userSalt.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
<?php

class m121001_001625_userSalt extends CDbMigration
{
public function safeUp()
{
if (!Yii::app()->getModule('user')) {
echo "\n\nAdd to console.php :\n"
."'modules'=>array(\n"
."...\n"
." 'user'=>array(\n"
." ... # copy settings from main config\n"
." ),\n"
."...\n"
."),\n"
."\n";
return false;
}

switch ($this->dbType()) {
case "mysql":
$this->addColumn(Yii::app()->getModule('user')->tableUsers,'salt',"VARCHAR(40) NULL DEFAULT NULL AFTER `password`");
break;
case "sqlite":
default:
$this->addColumn(Yii::app()->getModule('user')->tableUsers,'salt',"VARCHAR(40) DEFAULT NULL");
break;
}
}

public function safeDown()
{
switch ($this->dbType()) {
case "mysql":
$this->dropColumn(Yii::app()->getModule('user')->tableUsers,'salt');
break;
case "sqlite":

$this->execute('ALTER TABLE `'.Yii::app()->getModule('user')->tableUsers.'` RENAME TO `'.__CLASS__.'_'.Yii::app()->getModule('user')->tableUsers.'`');
$this->createTable(Yii::app()->getModule('user')->tableUsers, array(
"id" => "pk",
"username" => "varchar(20) NOT NULL",
"password" => "varchar(128) NOT NULL",
"email" => "varchar(128) NOT NULL",
"activkey" => "varchar(128) NOT NULL",
"superuser" => "int(1) NOT NULL",
"status" => "int(1) NOT NULL",
"create_at" => "TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP",
"lastvisit_at" => "TIMESTAMP",
));
$this->execute('INSERT INTO `'.Yii::app()->getModule('user')->tableUsers.'` SELECT "id","username","password","email","activkey","superuser","status","create_at","lastvisit_at" FROM `'.__CLASS__.'_'.Yii::app()->getModule('user')->tableUsers.'`');
$this->dropTable(__CLASS__.'_'.Yii::app()->getModule('user')->tableUsers);

break;
}
}

public function dbType()
{
list($type) = explode(':',Yii::app()->db->connectionString);
echo "type db: ".$type."\n";
return $type;
}
}
16 changes: 14 additions & 2 deletions models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class User extends CActiveRecord
* @var integer $id
* @var string $username
* @var string $password
* @var string $salt
* @var string $email
* @var string $activkey
* @var integer $createtime
Expand Down Expand Up @@ -121,7 +122,7 @@ public function scopes()
'condition'=>'superuser=1',
),
'notsafe'=>array(
'select' => 'id, username, password, email, activkey, create_at, lastvisit_at, superuser, status',
'select' => 'id, username, password, salt, email, activkey, create_at, lastvisit_at, superuser, status',
),
);
}
Expand Down Expand Up @@ -196,8 +197,19 @@ public function getLastvisit() {
public function setLastvisit($value) {
$this->lastvisit_at=date('Y-m-d H:i:s',$value);
}

/**
* simply returns the md5 digested value of PHP's uniqid()
* That should be sufficient to make most rainbowtables useless.
*
* Modify this function to change the way a salt is generated if you disagree.
*/
public static function getNewSalt()
{
return md5(uniqid(mt_rand(), true));
}

public function afterSave() {
public function afterSave() {
if (get_class(Yii::app())=='CWebApplication'&&Profile::$regMode==false) {
Yii::app()->user->updateSession();
}
Expand Down
5 changes: 4 additions & 1 deletion models/UserChangePassword.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ public function attributeLabels()
*/
public function verifyOldPassword($attribute, $params)
{
if (User::model()->notsafe()->findByPk(Yii::app()->user->id)->password != Yii::app()->getModule('user')->encrypting($this->$attribute))
$old_password = User::model()->notsafe()->findByPk(Yii::app()->user->id);
$old_salt = $old_password->salt;
$old_password = $old_password->password;
if ($old_password != Yii::app()->getModule('user')->encrypting($this->$attribute, $old_salt))
$this->addError($attribute, UserModule::t("Old Password is incorrect."));
}
}
2 changes: 0 additions & 2 deletions views/admin/view.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,7 @@
}

array_push($attributes,
'password',
'email',
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is actually unrelated to the merge request, but I think it's a small security improvement.

No one really has any business looking at user passwords, hashed or otherwise. Feel free to undo this change if you disagree, though.

'activkey',
'create_at',
'lastvisit_at',
array(
Expand Down