Skip to content

Commit

Permalink
fix: fixed a theoretical vuln where it was possible to guess the reco…
Browse files Browse the repository at this point in the history
…very key

PW recovery was also moved to the auth module
  • Loading branch information
Err0r1 committed Feb 2, 2023
1 parent d9e1bfa commit 4909c50
Show file tree
Hide file tree
Showing 15 changed files with 368 additions and 436 deletions.
10 changes: 3 additions & 7 deletions application/config/routes.php
Expand Up @@ -32,6 +32,9 @@
$route['login'] = 'auth/login';
$route['logout'] = 'auth/logout';
//$route['register'] = 'auth/register';
$route['password_recovery'] = 'auth/password_recovery';
$route['password_recovery/create_request'] = 'auth/password_recovery/create_request';
$route['password_recovery/reset_password'] = 'auth/password_recovery/reset_password';

// News
$route['news/(:num)'] = "news/index/$1";
Expand Down Expand Up @@ -63,12 +66,5 @@
// Vote
$route['vote/callback/(:any)'] = "vote/callback/index/$1";

// Forum
$route['forum/(:num)'] = "forum/getForum/$1";
$route['forum/(:num)/new'] = "forum/newTopic/$1";
$route['forum/(:num)/(:num)'] = "forum/getTopic/$1/$2";
$route['forum/(:num)/(:num)/remove/topic'] = "forum/removeTopic/$1/$2";
$route['forum/(:num)/(:num)/remove/post'] = "forum/removePost/$1/$2";

/* End of file routes.php */
/* Location: ./application/config/routes.php */
146 changes: 146 additions & 0 deletions application/modules/auth/controllers/Password_recovery.php
@@ -0,0 +1,146 @@
<?php

class Password_recovery extends MX_Controller
{
public function __construct()
{
parent::__construct();

$this->load->model('password_recovery_model');

$this->load->helper('email_helper');

$this->load->library('security');
$this->load->library('form_validation');

$this->user->guestArea();

requirePermission("view");

if (!$this->config->item('has_smtp'))
{
redirect('errors');
}
}

public function index()
{
clientLang("email_sent", "recovery");

$this->template->setTitle(lang("password_recovery", "recovery"));

$data = [];

$content = $this->template->loadPage("password_recovery.tpl", $data);
$box = $this->template->box(lang("password_recovery", "recovery"), $content);
$this->template->view($box, "modules/auth/css/recovery.css", "modules/auth/js/recovery.js");
}

public function create_request()
{
$this->form_validation->set_rules('email', 'email', 'trim|required|valid_email');

$this->form_validation->set_error_delimiters('', '');

$data = [
"messages" => false,
"success" => false
];

if ($this->form_validation->run())
{
//Check csrf
if ($this->input->post("token") != $this->security->get_csrf_hash())
{
$data['messages']["error"] = 'Something went wrong. Please reload the page.';
die(json_encode($data));
}

$email = $this->input->post("email");

if ($this->external_account_model->emailExists($email))
{
$username = $this->password_recovery_model->get_username($email);
$token = $this->generate_token($username, $email);

$link = base_url() . 'password_recovery/reset_password?token=' . $token;
sendMail($email, $this->config->item('server_name') . ': ' . lang("reset_password", "recovery"), $username, lang("email", "recovery") . ' <a href="' . $link . '">' . $link . '</a>', 1);

$this->password_recovery_model->insert_token($token, $username, $email, $this->input->ip_address());
$this->logger->createLog("user", "recovery", "Password recovery requested", [], Logger::STATUS_SUCCEED, $this->user->getId($this->input->post("username")));
}

$data['messages']["success"] = lang("email_sent", "recovery");
die(json_encode($data));
}
else
{
$data['messages']["error"] = validation_errors();
die(json_encode($data));
}
}

public function reset_password()
{
clientLang("password_changed", "recovery");

$this->form_validation->set_rules('token', 'token', 'trim|required');
$this->form_validation->set_rules('new_password', 'new_password', 'trim|required|min_length[6]');

$this->form_validation->set_error_delimiters('', '');

if ($this->input->method() === 'post')
{
if ($this->form_validation->run())
{
$new_password = $this->input->post('new_password');
$token = $this->input->post('token');
$token_data = $this->password_recovery_model->get_token($token);

if ($this->input->post("csrf_token") != $this->security->get_csrf_hash())
{
$data['messages']["error"] = 'Something went wrong. Please reload the page.';
die(json_encode($data));
}

if (!$token_data)
{
$data['messages']["error"] = lang('invalid', 'recovery');
die(json_encode($data));
}

$hash = $this->user->createHash($token_data['username'], $new_password);
$this->external_account_model->setPassword($token_data['username'], $hash["verifier"]);

$this->logger->createLog("user", "recovery", "Password changed via reset", [], Logger::STATUS_SUCCEED, $this->user->getId($token_data['username']));

$this->password_recovery_model->delete_token($token);

$data['messages']["success"] = lang("password_reset_success", "recovery");
die(json_encode($data));
}
else
{
$data['messages']["error"] = validation_errors();
die(json_encode($data));
}
}

$this->template->setTitle(lang("password_reset", "recovery"));

$data = [];
$data['token'] = $this->input->get('token');

$content = $this->template->loadPage("password_reset.tpl", $data);
$box = $this->template->box(lang("password_reset", "recovery"), $content);
$this->template->view($box, "modules/auth/css/recovery.css", "modules/auth/js/recovery.js");
}

private function generate_token($username, $email)
{
$timestamp = time();
$random_string = bin2hex(random_bytes(32));
$token = hash('sha512', $username . $email . $timestamp . $random_string);
return $token;
}
}
15 changes: 15 additions & 0 deletions application/modules/auth/css/recovery.css
@@ -0,0 +1,15 @@
.page-subbody {
margin: 0;
}

.is-fullwidth .page_form {
padding: 0;
}

.invalid-feedback {
color: #dc3545!important;
}

.valid-feedback {
color: #0f5132!important;
}
81 changes: 81 additions & 0 deletions application/modules/auth/js/recovery.js
@@ -0,0 +1,81 @@
var Recovery = {
timeout: null,
useCaptcha: false,

request: function() {
var postData = {
"email": $(".email-input").val(),
"csrf_token_name": Config.CSRF,
"token": Config.CSRF
};

clearTimeout (Recovery.timeout);
Recovery.timeout = setTimeout (function()
{
$.post(Config.URL + "password_recovery/create_request", postData, function(data) {
try {
data = JSON.parse(data);
console.log(data);

if(data["messages"]["error"]) {
if($(".email-input").val() != "") {
$(".error-feedback").addClass("invalid-feedback alert-danger d-block").removeClass("d-none").html(data["messages"]["error"]);
}
}
else if(data["messages"]["success"]) {
if($(".email-input").val() != "") {
$(".error-feedback").addClass("valid-feedback alert-success d-block").removeClass("d-none").html(data["messages"]["success"]);
$(".email-input").val('');
}
}

} catch(e) {
console.error(e);
console.log(data);
}
});

console.log(postData);

}, 500);
},

reset: function() {
var postData = {
"token": $(".token-input").val(),
"new_password": $(".password-input").val(),
"csrf_token_name": Config.CSRF,
"csrf_token": Config.CSRF
};

clearTimeout (Recovery.timeout);
Recovery.timeout = setTimeout (function()
{
$.post(Config.URL + "password_recovery/reset_password", postData, function(data) {
try {
data = JSON.parse(data);
console.log(data);

if(data["messages"]["error"]) {
if($(".password-input").val() != "") {
$(".error-feedback").addClass("invalid-feedback alert-danger d-block").removeClass("d-none").html(data["messages"]["error"]);
}
}
else if(data["messages"]["success"]) {
if($(".password-input").val() != "") {
$(".error-feedback").addClass("valid-feedback alert-success d-block").removeClass("invalid-feedback alert-danger d-none").html(data["messages"]["success"]);
$(".password-input, .token-input").val('');
}
}

} catch(e) {
console.error(e);
console.log(data);
}
});

console.log(postData);

}, 500);
}
}
18 changes: 18 additions & 0 deletions application/modules/auth/language/english/recovery.php
@@ -0,0 +1,18 @@
<?php

$lang['email'] = "E-Mail";
$lang['new_password'] = "New password";
$lang['token'] = "Token";
$lang['recover'] = "Recover password";
$lang['password_recovery'] = "Password recovery";
$lang['password_reset'] = "Reset password";
$lang['password_reset_success'] = "The password has been successfully reset";
$lang['password_changed'] = "Password reseted";
$lang['email_sent'] = "If your E-Mail address exists in our system, an E-Mail has been sent to your registered E-Mail address. Please check your inbox to proceed.";
$lang['reset_password'] = "Reset your password";
$lang['email_text'] = "You have requested to reset your password, to complete the request please navigate to ";
$lang['go_back'] = "Go back";
$lang['invalid'] = "Invalid token.";
$lang['error_while_inserting'] = "Error while inserting the token.";
$lang['lost_password'] = "Lost password?";
$lang['enter_your_email'] = "Enter your E-Mail below and we'll send you a link to reset your password.";
76 changes: 76 additions & 0 deletions application/modules/auth/models/Password_recovery_model.php
@@ -0,0 +1,76 @@
<?php

class Password_recovery_model extends CI_Model
{
private $connection;

public function __construct()
{
if (empty($this->connection))
{
$this->connection = $this->load->database("account", true);
}
}

public function get_username($email)
{
$this->connection->select(column("account", "username"));
$this->connection->from(table("account"));
$this->connection->where(column("account", "email"), $email);
$query = $this->connection->get();
$result = $query->result_array();
return $result[0][column("account", "username")];
}

public function get_token($token)
{
if ($token)
{
$this->db->select("*");
$this->db->from("password_recovery_key");
$this->db->where("recoverykey", $token);
$query = $this->db->get();

$result = $query->result_array();
if (isset($result[0]["recoverykey"]) == $token)
{
return $result[0];
} else {
return false;
}
} else {
return false;
}
}

public function insert_token($token, $username, $email, $ip)
{
if (!$token || !$username || !$email || !$ip)
{
return false;
}

$data = [
"recoverykey" => $token,
"username" => $username,
"email" => $email,
"ip" => $ip,
"time" => time(),
];

$this->db->insert("password_recovery_key", $data);
return true;
}

public function delete_token($token)
{
if (!$token)
{
return false;
}

$this->db->where("recoverykey", $token);
$this->db->delete("password_recovery_key");
return true;
}
}
14 changes: 14 additions & 0 deletions application/modules/auth/views/password_recovery.tpl
@@ -0,0 +1,14 @@
<div class="page-subbody">
<div class="page_form">
<h4 class="font-weight-semibold text-uppercase mb-0">{lang('lost_password', 'recovery')}</h4>
<p class="text-2 opacity-7">{lang('enter_your_email', 'recovery')}</p>
<form onSubmit="Recovery.request(); return false">
<div class="alert text-center error-feedback d-none" role="alert"></div>

<label>{lang('email', 'recovery')}</label>
<input type="email" class="email-input mb-3" required>

<input type="submit" value="{lang('recover', 'recovery')}" class="nice_button text-center">
</form>
</div>
</div>
15 changes: 15 additions & 0 deletions application/modules/auth/views/password_reset.tpl
@@ -0,0 +1,15 @@
<div class="page-subbody">
<div class="page_form">
<form onSubmit="Recovery.reset(); return false">
<div class="alert text-center error-feedback d-none" role="alert"></div>

<label>{lang('token', 'recovery')}</label>
<input type="text" value="{$token}" class="token-input mb-3" required>

<label>{lang('new_password', 'recovery')}</label>
<input type="password" class="password-input mb-3"required>

<input type="submit" value="{lang('reset_password', 'recovery')}" class="nice_button text-center">
</form>
</div>
</div>

0 comments on commit 4909c50

Please sign in to comment.