From 4909c506a58f208a86f15676039c3bceeb02ee53 Mon Sep 17 00:00:00 2001 From: Err0r <74456323+Err0r1@users.noreply.github.com> Date: Thu, 2 Feb 2023 21:50:48 +0100 Subject: [PATCH] fix: fixed a theoretical vuln where it was possible to guess the recovery key PW recovery was also moved to the auth module --- application/config/routes.php | 10 +- .../auth/controllers/Password_recovery.php | 146 ++++++++++++++++ application/modules/auth/css/recovery.css | 15 ++ application/modules/auth/js/recovery.js | 81 +++++++++ .../auth/language/english/recovery.php | 18 ++ .../auth/models/Password_recovery_model.php | 76 ++++++++ .../modules/auth/views/password_recovery.tpl | 14 ++ .../modules/auth/views/password_reset.tpl | 15 ++ .../controllers/Password_recovery.php | 165 ------------------ .../password_recovery/css/recovery.css | 69 -------- .../modules/password_recovery/js/recovery.js | 17 -- .../language/english/recovery.php | 18 -- .../modules/password_recovery/manifest.json | 29 --- .../models/Password_recovery_model.php | 108 ------------ .../views/password_recovery.tpl | 23 --- 15 files changed, 368 insertions(+), 436 deletions(-) create mode 100644 application/modules/auth/controllers/Password_recovery.php create mode 100644 application/modules/auth/css/recovery.css create mode 100644 application/modules/auth/js/recovery.js create mode 100644 application/modules/auth/language/english/recovery.php create mode 100644 application/modules/auth/models/Password_recovery_model.php create mode 100644 application/modules/auth/views/password_recovery.tpl create mode 100644 application/modules/auth/views/password_reset.tpl delete mode 100644 application/modules/password_recovery/controllers/Password_recovery.php delete mode 100644 application/modules/password_recovery/css/recovery.css delete mode 100644 application/modules/password_recovery/js/recovery.js delete mode 100644 application/modules/password_recovery/language/english/recovery.php delete mode 100644 application/modules/password_recovery/manifest.json delete mode 100644 application/modules/password_recovery/models/Password_recovery_model.php delete mode 100644 application/modules/password_recovery/views/password_recovery.tpl diff --git a/application/config/routes.php b/application/config/routes.php index 4d1e59eb..3ffbc4f7 100644 --- a/application/config/routes.php +++ b/application/config/routes.php @@ -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"; @@ -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 */ diff --git a/application/modules/auth/controllers/Password_recovery.php b/application/modules/auth/controllers/Password_recovery.php new file mode 100644 index 00000000..51a45611 --- /dev/null +++ b/application/modules/auth/controllers/Password_recovery.php @@ -0,0 +1,146 @@ +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") . ' ' . $link . '', 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; + } +} diff --git a/application/modules/auth/css/recovery.css b/application/modules/auth/css/recovery.css new file mode 100644 index 00000000..ae62a610 --- /dev/null +++ b/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; +} \ No newline at end of file diff --git a/application/modules/auth/js/recovery.js b/application/modules/auth/js/recovery.js new file mode 100644 index 00000000..970839aa --- /dev/null +++ b/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); + } +} \ No newline at end of file diff --git a/application/modules/auth/language/english/recovery.php b/application/modules/auth/language/english/recovery.php new file mode 100644 index 00000000..1380ad6c --- /dev/null +++ b/application/modules/auth/language/english/recovery.php @@ -0,0 +1,18 @@ +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; + } +} diff --git a/application/modules/auth/views/password_recovery.tpl b/application/modules/auth/views/password_recovery.tpl new file mode 100644 index 00000000..ab819550 --- /dev/null +++ b/application/modules/auth/views/password_recovery.tpl @@ -0,0 +1,14 @@ +
+
+

{lang('lost_password', 'recovery')}

+

{lang('enter_your_email', 'recovery')}

+
+ + + + + + +
+
+
diff --git a/application/modules/auth/views/password_reset.tpl b/application/modules/auth/views/password_reset.tpl new file mode 100644 index 00000000..0aefed7c --- /dev/null +++ b/application/modules/auth/views/password_reset.tpl @@ -0,0 +1,15 @@ +
+
+
+ + + + + + + + + +
+
+
diff --git a/application/modules/password_recovery/controllers/Password_recovery.php b/application/modules/password_recovery/controllers/Password_recovery.php deleted file mode 100644 index 9e485065..00000000 --- a/application/modules/password_recovery/controllers/Password_recovery.php +++ /dev/null @@ -1,165 +0,0 @@ -load->model('password_recovery_model'); - - $this->load->helper('email_helper'); - - $this->user->guestArea(); - - requirePermission("view"); - - if (!$this->config->item('has_smtp')) { - die(lang("smtp_disabled", "recovery")); - } - } - - public function index() - { - clientLang("email_sent", "recovery"); - - $this->template->setTitle(lang("password_recovery", "recovery")); - - $data = array(); - - $content = $this->template->loadPage("password_recovery.tpl", $data); - $box = $this->template->box(lang("password_recovery", "recovery"), $content); - $this->template->view($box, "modules/password_recovery/css/recovery.css", "modules/password_recovery/js/recovery.js"); - } - - public function createRequest($acc = false) - { - // timestamp | 10min delay for next request or captcha? - if (!$acc || empty($acc)) { - die('No account'); - } - - if ($acc) { - $email = $this->password_recovery_model->getEmail($acc); - - $link = base_url() . 'password_recovery/requestPassword/' . $this->generateKey($acc, $email); - sendMail($email, $this->config->item('server_name') . ': ' . lang("reset_password", "recovery"), $acc, lang("email", "recovery") . ' ' . $link . '', 1); - } - - die('yes'); - } - - public function requestPassword($key = "") - { - $key || $this->template->box(breadcumb(array( - 'password_recovery' => lang('password_recovery', 'recovery'), - )), lang('no_key', 'recovery'), true); - //var_dump ($key); - - $encryption = $this->realms->getEmulator()->encryption(); - //var_dump ($encryption); - - switch ($encryption) { - case 'SHP': - $username = $this->password_recovery_model->getKey($key); - - // Make sure that it is the right key - $username || $this->template->box(breadcumb(array( - 'password_recovery' => lang('password_recovery', 'recovery'), - )), lang('invalid', 'recovery'), true); - - $password = $this->generatePassword(); //New password - - //Hash password for the database - $newPasswordHash = sha1(strtoupper($username) . ':' . strtoupper($password)); - - //Change the password - $this->password_recovery_model->changePassword($username, $newPasswordHash); - - //Send a mail with the new password - sendMail($this->password_recovery_model->getEmail($username), $this->config->item('server_name') . ': ' . lang("your_new_password", "recovery"), $username, lang("new_password", "recovery") . ' ' . $newPasswordHash . '', 1); - - //Show a new message - $this->template->view($this->template->loadPage("page.tpl", array( - "module" => "default", - "headline" => lang("password_recovery", "recovery"), - "content" => lang("changed", "recovery") - ))); - - //Remove the key from the database - $this->password_recovery_model->deletekey($key); - break; - case 'SRP6': - case 'HEX': - $username = $this->password_recovery_model->getKey($key); - - // Make sure that it is the right key - $username || $this->template->box(breadcumb(array( - 'password_recovery' => lang('password_recovery', 'recovery'), - )), lang('invalid', 'recovery'), true); - - $password = $this->generatePassword(); //New password - $PW = $this->user->createHash($username, $password); - - // Hash the password through the current active emulator and set it for the user - $this->password_recovery_model->changePassword_srp6($username, $PW['verifier']); - - // Send a mail with the new password - sendMail($this->password_recovery_model->getEmail($username), $this->config->item('server_name') . ': ' . lang('your_new_password', 'recovery'), $username, lang('new_password', 'recovery') . ' ' . $password . '', 1); - - // Remove the key from the database - $this->password_recovery_model->deletekey($key); - - $this->template->box(breadcumb(array( - 'password_recovery' => lang('password_recovery', 'recovery'), - )), lang('changed', 'recovery'), true); - break; - default: - $this->template->view($this->template->loadPage("page.tpl", array( - "module" => "default", - "headline" => lang("password_recovery", "recovery"), - "content" => lang("invalid", "recovery") - ))); - break; - } - } - - private function generateKey($username, $email) - { - $encryption = $this->realms->getEmulator()->encryption(); - - switch ($encryption) { - case 'SHP': - $key = sha1($username . ":" . $email . ":" . time()); - - if ($this->password_recovery_model->insertKey($key, $username, $email, $this->input->ip_address())) { - return $key; // a new key has been generated for the user - } - - $this->template->box(breadcumb(array( - 'password_recovery' => lang('password_recovery', 'recovery'), - )), lang('error_while_inserting', 'recovery'), true); - break; - case 'SRP6': - case 'HEX': - $key = sha1($username . ':' . $email . ':' . time()); - - if ($this->password_recovery_model->insertKey($key, $username, $email, $this->input->ip_address())) { - return $key; // a new key has been generated for the user - } - - $this->template->box(breadcumb(array( - 'password_recovery' => lang('password_recovery', 'recovery'), - )), lang('error_while_inserting', 'recovery'), true); - break; - default: - die(); - break; - } - } - - private function generatePassword() - { - return substr(sha1(time()), 0, 10); - } -} diff --git a/application/modules/password_recovery/css/recovery.css b/application/modules/password_recovery/css/recovery.css deleted file mode 100644 index b5139c29..00000000 --- a/application/modules/password_recovery/css/recovery.css +++ /dev/null @@ -1,69 +0,0 @@ -/* Featured Box */ -.featured-box { - background: #1b1d29; - box-sizing: border-box; - border-bottom: 1px solid #13151e; - border-left: 1px solid #13151e; - border-radius: 8px; - border-right: 1px solid #13151e; - box-shadow: 0 2px 4px 0px rgba(0, 0, 0, 0.05); - margin-bottom: 20px; - margin-left: auto; - margin-right: auto; - margin-top: 20px; - min-height: 100px; - position: relative; - text-align: center; - z-index: 1; -} - -.featured-box h4 { - font-size: 1.3em; - font-weight: 400; - letter-spacing: -0.7px; - margin-top: 5px; - margin-bottom: 5px; -} - -.featured-box .box-content { - border-radius: 8px; - border-top: 1px solid rgba(0, 0, 0, 0.06); - border-top-width: 4px; - padding: 28.8px; - padding: 1.8rem; - position: relative; -} - -.featured-box .box-content:not(.box-content-border-0) { - top: -1px; - border-top-width: 4px; -} - -.featured-box .box-content.box-content-border-0 { - border-top: 1px solid rgba(0, 0, 0, 0.06) !important; - border-bottom: 0 !important; -} - -.featured-box .box-content.box-content-border-bottom { - top: 1px; -} - -.featured-box .box-content-border-bottom { - border-top: 1px solid rgba(0, 0, 0, 0.06) !important; - border-bottom: 4px solid rgba(0, 0, 0, 0.06); -} - -.featured-box.border-radius { - border-radius: 8px !important; -} - -.featured-box.border-radius.box-shadow-1:before { - border-radius: 8px !important; -} -.featured-box-primary .box-content { - border-top-color: #0088CC; -} - -.featured-box-primary .box-content-border-bottom { - border-bottom-color: #0088CC; -} \ No newline at end of file diff --git a/application/modules/password_recovery/js/recovery.js b/application/modules/password_recovery/js/recovery.js deleted file mode 100644 index b102285b..00000000 --- a/application/modules/password_recovery/js/recovery.js +++ /dev/null @@ -1,17 +0,0 @@ -var Recovery = { - get: function() - { - var value = {csrf_token_name: Config.CSRF}; - var account = $("input#recovery").val(); - - value['account'] = account; - - $.post(Config.URL + "password_recovery/createRequest/" + account, value, function(response) - { - Swal.fire({ - icon: "success", - text: lang("email_sent", "recovery"), - }); - }) - } -} \ No newline at end of file diff --git a/application/modules/password_recovery/language/english/recovery.php b/application/modules/password_recovery/language/english/recovery.php deleted file mode 100644 index 58408795..00000000 --- a/application/modules/password_recovery/language/english/recovery.php +++ /dev/null @@ -1,18 +0,0 @@ -connection)) { - $this->connection = $this->load->database("account", true); - } - } - - public function getEmail($username) - { - if (!$username) { - return false; - } - - $query = $this->external_account_model->getConnection()->query(sprintf( - 'SELECT %s FROM %s WHERE %s = ?', - column('account', 'email'), - table('account'), - column('account', 'username') - ), [$username]); - - if (!$query->num_rows()) { - return false; - } - - return $query->row()->email; - } - - public function existsEmail($email) - { - if (!$email) { - return false; - } else { - $query = $this->connection->query("SELECT " . column("account", "id") . " FROM " . table("account") . " WHERE " . column("account", "email") . "= ?", array($email)); - - if ($query->num_rows() > 0) { - return true; - } else { - return false; - } - } - } - - - public function changePassword($username, $newPassword) - { - if ($username && $newPassword) { - $this->connection->query("UPDATE " . table("account") . " SET " . column("account", "password") . " = ?, " . column("account", "sessionkey") . " = '', " . column("account", "v") . " = '', " . column("account", "s") . " = '' WHERE " . column("account", "username") . " = ?", array($newPassword, $username)); - } else { - return false; - } - } - - public function changePassword_srp6($username, $newPassword) - { - if (!$username || !$newPassword) { - return false; - } - - if (column('account', 'v') && column('account', 's') && column('account', 'sessionkey')) { - $this->external_account_model->getConnection()->set(column('account', 'sessionkey'), '') - ->set(column('account', 'salt'), '')->set(column('account', 'verifier'), ''); - } - - $bla = $this->external_account_model->getConnection()->set(column('account', 'password'), $newPassword) - ->where(column('account', 'username'), $username)->update(table('account')); - } - - public function getKey($key) - { - if ($key) { - $query = $this->db->query("SELECT recoverykey, username FROM password_recovery_key WHERE recoverykey = ?", array($key)); - $result = $query->result_array(); - if (isset($result[0]['recoverykey']) == $key) { - return $result[0]['username']; - } else { - return false; - } - } else { - return false; - } - } - - public function insertKey($key, $username, $email, $ip) - { - if (!$key || !$ip || !$username) { - return false; - } - - $this->db->query("INSERT INTO password_recovery_key VALUES (?, ?, ?, ?, ?)", [$key, $username, $email, $ip, time()]); - return true; - } - - public function deleteKey($key) - { - if (!$key) { - return false; - } - - $this->db->query('DELETE FROM password_recovery_key WHERE recoverykey = ?', [$key]); - return true; - } -} diff --git a/application/modules/password_recovery/views/password_recovery.tpl b/application/modules/password_recovery/views/password_recovery.tpl deleted file mode 100644 index 7d7579b0..00000000 --- a/application/modules/password_recovery/views/password_recovery.tpl +++ /dev/null @@ -1,23 +0,0 @@ -
-
- -
-
\ No newline at end of file