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

Prevent password reset brute force #778

Open
wants to merge 7 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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions application/config/texts.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,11 @@
"FEEDBACK_AVATAR_FOLDER_DOES_NOT_EXIST_OR_NOT_WRITABLE" => "Avatar folder does not exist or is not writable. Please change this via chmod 775 or 777.",
"FEEDBACK_AVATAR_IMAGE_UPLOAD_FAILED" => "Something went wrong with the image upload.",
"FEEDBACK_AVATAR_IMAGE_DELETE_SUCCESSFUL" => "You successfully deleted your avatar.",
"FEEDBACK_AVATAR_IMAGE_DELETE_NO_FILE" => "You don't have a custom avatar.",
"FEEDBACK_AVATAR_IMAGE_DELETE_FAILED" => "Something went wrong while deleting your avatar.",
"FEEDBACK_AVATAR_IMAGE_DELETE_NO_FILE" => "You don't have a custom avatar.",
"FEEDBACK_AVATAR_IMAGE_DELETE_FAILED" => "Something went wrong while deleting your avatar.",
"FEEDBACK_PASSWORD_RESET_TOKEN_FAIL" => "Could not write token to database.",
"FEEDBACK_PASSWORD_RESET_TOKEN_MISSING" => "No password reset token.",
"FEEDBACK_PASSWORD_RESET_TOKEN_INVALID" => "Invalid verification code. Any existing password reset requests have been canceled. Please request a new reset link.",
"FEEDBACK_PASSWORD_RESET_MAIL_SENDING_ERROR" => "Password reset mail could not be sent due to: ",
"FEEDBACK_PASSWORD_RESET_MAIL_SENDING_SUCCESSFUL" => "A password reset mail has been sent successfully.",
"FEEDBACK_PASSWORD_RESET_LINK_EXPIRED" => "Your reset link has expired. Please use the reset link within one hour.",
Expand Down
60 changes: 52 additions & 8 deletions application/model/PasswordResetModel.php
Original file line number Diff line number Diff line change
Expand Up @@ -128,27 +128,40 @@ public static function verifyPasswordReset($user_name, $verification_code)
$database = DatabaseFactory::getFactory()->getConnection();

// check if user-provided username + verification code combination exists
$sql = "SELECT user_id, user_password_reset_timestamp
$sql = "SELECT user_id, user_password_reset_hash, user_password_reset_timestamp
FROM users
WHERE user_name = :user_name
AND user_password_reset_hash = :user_password_reset_hash
AND user_provider_type = :user_provider_type
LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(
':user_password_reset_hash' => $verification_code, ':user_name' => $user_name,
':user_name' => $user_name,
':user_provider_type' => 'DEFAULT'
));

// if this user with exactly this verification hash code does NOT exist
if ($query->rowCount() != 1) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_COMBINATION_DOES_NOT_EXIST'));
//if username does not exist
if($query->rowCount() != 1) {
Session::add('feedback_negative', Text::get('FEEDBACK_USER_DOES_NOT_EXIST'));
return false;
}

// get result row (as an object)
$result_user_row = $query->fetch();


// if this user's verification hash code does NOT exist
if ($result_user_row->user_password_reset_hash == NULL) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_TOKEN_INVALID'));
return false;
}

// if this user's verification hash code does not match
// marks existing request as expired
if ($result_user_row->user_password_reset_hash != $verification_code) {
Session::add('feedback_negative', Text::get('FEEDBACK_PASSWORD_RESET_TOKEN_INVALID'));
self::expirePasswordReset($user_name);
return false;
}

// 3600 seconds are 1 hour
$timestamp_one_hour_ago = time() - 3600;

Expand All @@ -162,6 +175,37 @@ public static function verifyPasswordReset($user_name, $verification_code)
return false;
}
}

/**
* Marks existing password reset request as expired to prevent prevent guessing hash codes
* @param string $user_name Username
* @return bool Success status
*/
private static function expirePasswordReset($user_name) {
// 3600 seconds are 1 hour
$timestamp_one_hour_ago = time() - 3600;

$database = DatabaseFactory::getFactory()->getConnection();

$sql = "UPDATE users
SET user_password_reset_timestamp = :user_password_reset_timestamp
WHERE user_name = :user_name AND user_provider_type = :provider_type LIMIT 1";
$query = $database->prepare($sql);
$query->execute(array(
':user_name' => $user_name,
':user_password_reset_timestamp' => $timestamp_one_hour_ago,
':provider_type' => 'DEFAULT'
));

// check if exactly one row was successfully changed
if ($query->rowCount() == 1) {
return true;
}

// user feedback not necessary here
// but adding a note in the logs may be useful
return false;
}

/**
* Writes the new password to the database
Expand Down