diff --git a/application/config/texts.php b/application/config/texts.php index 78c21ea13..dc9166cb7 100644 --- a/application/config/texts.php +++ b/application/config/texts.php @@ -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.", diff --git a/application/model/PasswordResetModel.php b/application/model/PasswordResetModel.php index 67b602bd2..a39d01a48 100644 --- a/application/model/PasswordResetModel.php +++ b/application/model/PasswordResetModel.php @@ -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; @@ -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