[TikiWiki-commits] [Git][tikiwiki/tiki][master] [EHN] Tiki reset password: Use random token for secure password reset
Benoit Grégoire (@benoitg) via TikiWiki-cvs <[email protected]>
| Newsgroups | gmane.comp.cms.tiki.cvs |
|---|---|
| Message-ID | <68b85fee2ca6c_2cdd054570f0@gitlab-sidekiq-low-urgency-cpu-bound-v2-84cbfb8f78-gbwjq.mail> |
Benoit Grégoire pushed to branch master at Tiki Wiki CMS Groupware / Tiki
Commits:
a88d01eb by Espoir Baraka at 2025-09-03T15:25:13+00:00
[EHN] Tiki reset password: Use random token for secure password reset
---
* [EHN] Tiki reset password: Use random token for secure password reset
See merge request tikiwiki/tiki!8426
- - - - -
8 changed files:
- db/tiki.sql
- db/tiki_convert_myisam_to_innodb.sql
- + installer/schema/20250107_add_password_reset_tokens_tiki.sql
- + lib/Auth/PasswordResetLib.php
- templates/mail/password_reminder.tpl
- templates/tiki-change_password.tpl
- tiki-change_password.php
- tiki-remind_password.php
Changes:
=====================================
db/tiki.sql
=====================================
@@ -4147,4 +4147,18 @@ CREATE TABLE `tiki_user_passwords_history` (
`hash` varchar(60) default NULL,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY(`passId`)
+) ENGINE=MyISAM;
+
+DROP TABLE IF EXISTS `tiki_password_reset_tokens`;
+CREATE TABLE `tiki_password_reset_tokens` (
+ `tokenId` int(11) NOT NULL AUTO_INCREMENT,
+ `user` varchar(200) NOT NULL,
+ `token` varchar(64) NOT NULL,
+ `created` int NOT NULL,
+ `expires` int NOT NULL,
+ `used` tinyint(1) NOT NULL DEFAULT 0,
+ PRIMARY KEY (`tokenId`),
+ UNIQUE KEY `token` (`token`),
+ KEY `user` (`user`),
+ KEY `expires` (`expires`)
) ENGINE=MyISAM;
\ No newline at end of file
=====================================
db/tiki_convert_myisam_to_innodb.sql
=====================================
@@ -277,4 +277,5 @@ ALTER TABLE `tiki_2fa_email_tokens` ENGINE = InnoDB;
ALTER TABLE `tiki_sql_query_logs` ENGINE = InnoDB;
ALTER TABLE `tiki_iot_apps` ENGINE = InnoDB;
ALTER TABLE `tiki_iot_apps_actions_logs` ENGINE=InnoDB;
-ALTER TABLE `tiki_user_passwords_history` ENGINE = InnoDB;
\ No newline at end of file
+ALTER TABLE `tiki_user_passwords_history` ENGINE = InnoDB;
+ALTER TABLE `tiki_password_reset_tokens` ENGINE = InnoDB;
\ No newline at end of file
=====================================
installer/schema/20250107_add_password_reset_tokens_tiki.sql
=====================================
@@ -0,0 +1,12 @@
+CREATE TABLE IF NOT EXISTS `tiki_password_reset_tokens` (
+ `tokenId` int(11) NOT NULL AUTO_INCREMENT,
+ `user` varchar(200) NOT NULL,
+ `token` varchar(64) NOT NULL,
+ `created` int NOT NULL,
+ `expires` int NOT NULL,
+ `used` tinyint(1) NOT NULL DEFAULT 0,
+ PRIMARY KEY (`tokenId`),
+ UNIQUE KEY `token` (`token`),
+ KEY `user` (`user`),
+ KEY `expires` (`expires`)
+) ENGINE=MyISAM;
\ No newline at end of file
=====================================
lib/Auth/PasswordResetLib.php
=====================================
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * @package tikiwiki
+ */
+
+// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
+//
+// All Rights Reserved. See copyright.txt for details and a complete list of authors.
+// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
+
+namespace Tiki\Lib\Auth;
+
+/**
+ * Library for secure password reset functionality
+ */
+class PasswordResetLib extends \TikiLib
+{
+ /**
+ * Generate a secure password reset token and store it in the database
+ * @param string $user Username
+ * @return array Array containing token and expiration time
+ */
+ public function generateSecurePasswordResetToken($user)
+ {
+ global $prefs;
+
+ // Clean up expired tokens first
+ $this->cleanupExpiredPasswordResetTokens();
+
+ // Generate a cryptographically secure random token
+ $token = bin2hex(random_bytes(32)); // 32 bytes = 64 hex characters
+
+ // Set expiration time (default to 1 hour if not configured)
+ $expiry_time = ($prefs['resetpasswordlink_expiry'] ?? 60) * 60;
+ $expires = time() + $expiry_time;
+
+ // Store the token in the database
+ $query = 'INSERT INTO `tiki_password_reset_tokens` (`user`, `token`, `created`, `expires`) VALUES (?, ?, ?, ?)';
+ $result = $this->query($query, [$user, $token, time(), $expires]);
+
+ if ($result) {
+ return [
+ 'token' => $token,
+ 'expires' => $expires,
+ 'expiry_time' => $expiry_time
+ ];
+ }
+
+ return false;
+ }
+
+ /**
+ * Validate a password reset token
+ * @param string $user Username
+ * @param string $token Reset token
+ * @return array|false Array with token info if valid, false otherwise
+ */
+ public function validatePasswordResetToken($user, $token)
+ {
+ // Clean up expired tokens first
+ $this->cleanupExpiredPasswordResetTokens();
+
+ $query = 'SELECT * FROM `tiki_password_reset_tokens` WHERE `user` = ? AND `token` = ? AND `expires` > ? AND `used` = 0';
+ $result = $this->query($query, [$user, $token, time()]);
+
+ if ($result && $result->numRows() > 0) {
+ return $result->fetchRow();
+ }
+
+ return false;
+ }
+
+ /**
+ * Mark a password reset token as used
+ * @param string $user Username
+ * @param string $token Reset token
+ * @return bool Success status
+ */
+ public function markPasswordResetTokenUsed($user, $token)
+ {
+ $query = 'UPDATE `tiki_password_reset_tokens` SET `used` = 1 WHERE `user` = ? AND `token` = ?';
+ $result = $this->query($query, [$user, $token]);
+
+ return $result !== false;
+ }
+
+ /**
+ * Clean up expired password reset tokens
+ */
+ private function cleanupExpiredPasswordResetTokens()
+ {
+ $query = 'DELETE FROM `tiki_password_reset_tokens` WHERE `expires` < ?';
+ $this->query($query, [time()]);
+ }
+}
=====================================
templates/mail/password_reminder.tpl
=====================================
@@ -3,9 +3,11 @@
{tr _0=$prefs.mail_template_custom_text}Someone requested a password reset for your %0account{/tr} ({$mail_site}).
{tr}Please click on the following link to confirm you wish to reset your password and go to the screen where you must enter a new "permanent" password. Please pick a password only you will know, and don't share it with anyone else.{/tr}
-{mailurl}tiki-change_password.php?user={$mail_user|escape:'url'}&actpass={$mail_apass|escape:'url'}&ts={$mail_timestamp|escape:'url'}&hash={$mail_timestamp_hash|escape:'url'}{/mailurl}
+{mailurl}tiki-change_password.php?user={$mail_user|escape:'url'}&token={$mail_token|escape:'url'}&actpass={$mail_apass|escape:'url'}{/mailurl}
{tr}Important: Username & password are CaSe SenSitiVe{/tr}
{tr}Important: The old password remains active if you don't click the link above.{/tr}
+{tr}This reset link will expire in {$mail_expiry_time_formatted}.{/tr}
+
=====================================
templates/tiki-change_password.tpl
=====================================
@@ -21,8 +21,8 @@
<input type="hidden" name="apass" value="{$password|escape}">
<input type="hidden" name="timestamp" value="{$timestamp|escape}">
{/if}
- {if !empty($hash)}
- <input type="hidden" name="hash" value="{$hash|escape}">
+ {if !empty($secure_token)}
+ <input type="hidden" name="token" value="{$secure_token|escape}">
{/if}
<div class="card-header text-center">
{if $new_user_validation neq 'y'}
=====================================
tiki-change_password.php
=====================================
@@ -9,15 +9,17 @@
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
$inputConfiguration = [
- [ 'staticKeyFilters' => [
- 'user' => 'text',
- 'username' => 'text',
- 'pass' => 'none',
- 'passAgain' => 'none',
- 'oldpass' => 'none',
- 'change' => 'text',
+ [
+ 'staticKeyFilters' => [
+ 'user' => 'text',
+ 'username' => 'text',
+ 'pass' => 'none',
+ 'passAgain' => 'none',
+ 'oldpass' => 'none',
+ 'change' => 'text',
+ 'token' => 'text',
+ ],
],
- ]
];
require_once('tiki-setup.php');
@@ -31,11 +33,8 @@ if (! isset($_REQUEST["oldpass"])) {
$_REQUEST["oldpass"] = '';
}
-// Expected hash
$user = $_REQUEST["user"];
-$timestamp = $_REQUEST["timestamp"];
-$actpass = $_REQUEST["apass"];
-$expected_hash = md5($user . '|' . $actpass . '|' . $timestamp);
+$secure_token = $_REQUEST["token"] ?? '';
if (isset($_REQUEST["newuser"]) && $_REQUEST["newuser"] == 'y') {
$smarty->assign('new_user_validation', 'y');
@@ -43,26 +42,27 @@ if (isset($_REQUEST["newuser"]) && $_REQUEST["newuser"] == 'y') {
$smarty->assign('userlogin', $_REQUEST["user"]);
$smarty->assign('oldpass', $_REQUEST["oldpass"]);
-$smarty->assign('password', $_REQUEST["actpass"]);
-$smarty->assign('timestamp', $_REQUEST["ts"]);
-$smarty->assign('hash', $_REQUEST["hash"]);
+$smarty->assign('secure_token', $secure_token);
if (isset($_REQUEST["change"])) {
$access->checkCsrf();
- // If this is a new user validation, we do not check the hash or timestamp
+ // If this is a new user validation, we do not check the token
if (! isset($_REQUEST["new_user_validation"]) && $_REQUEST["new_user_validation"] !== 'y') {
- // Check if the hash is valid
- $provided_hash = $_REQUEST["hash"];
- if ($expected_hash !== $provided_hash) {
- Feedback::errorAndDie(tra("Invalid hash."), \Laminas\Http\Response::STATUS_CODE_403);
+ // Check if the secure token is valid
+ if (empty($secure_token)) {
+ Feedback::errorAndDie(tra("Missing reset token."), \Laminas\Http\Response::STATUS_CODE_400);
}
- // Check if the timestamp is valid
- $resetTime = $prefs['resetpasswordlink_expiry'];
- if (time() - (int)$timestamp > $resetTime) {
- Feedback::errorAndDie(tra("The link has expired."), \Laminas\Http\Response::STATUS_CODE_410);
+ $passwordResetLib = new \Tiki\Lib\Auth\PasswordResetLib();
+ $token_info = $passwordResetLib->validatePasswordResetToken($user, $secure_token);
+
+ if (! $token_info) {
+ Feedback::errorAndDie(tra("Invalid or expired reset token."), \Laminas\Http\Response::STATUS_CODE_403);
}
+
+ // Mark the token as used to prevent reuse
+ $passwordResetLib->markPasswordResetTokenUsed($user, $secure_token);
}
// Check that pass and passAgain match, otherwise display error and exit
=====================================
tiki-remind_password.php
=====================================
@@ -81,17 +81,36 @@ if (isset($_REQUEST["remind"])) {
include_once('lib/webmail/tikimaillib.php');
$name = $_REQUEST['name'];
- $pass = md5($userlib->renew_user_password($name));
- $timestamp = time();
- $hash = md5($name . '|' . $pass . '|' . $timestamp);
+ // Generate a secure password reset token instead of the insecure hash
+ $passwordResetLib = new \Tiki\Lib\Auth\PasswordResetLib();
+ $token_info = $passwordResetLib->generateSecurePasswordResetToken($name);
+
+ if (! $token_info) {
+ Feedback::errorAndDie(tra("Failed to generate password reset token. Please try again."), \Laminas\Http\Response::STATUS_CODE_500);
+ }
+
+ // Generate actpass for backward compatibility
+ $actpass = md5($userlib->renew_user_password($name));
+
+ // Format expiry time for display
+ $expiry_time_formatted = $token_info['expiry_time'];
+ // Convert seconds to minutes for display
+ $expiry_minutes = round($expiry_time_formatted / 60, 1);
+ if ($expiry_minutes < 1) {
+ $expiry_time_formatted = $expiry_time_formatted . ' ' . tr($expiry_time_formatted == 1 ? 'second' : 'seconds');
+ } else {
+ $expiry_time_formatted = $expiry_minutes . ' ' . tr($expiry_minutes == 1 ? 'minute' : 'minutes');
+ }
$languageEmail = $tikilib->get_user_preference($name, "language", $prefs['site_language']);
// Now check if the user should be notified by email
$smarty->assign('mail_site', $_SERVER["SERVER_NAME"]);
$smarty->assign('mail_user', $name);
- $smarty->assign('mail_timestamp', $timestamp);
- $smarty->assign('mail_timestamp_hash', $hash);
- $smarty->assign('mail_apass', $pass);
+ $smarty->assign('mail_token', $token_info['token']);
+ $smarty->assign('mail_expires', $token_info['expires']);
+ $smarty->assign('mail_expiry_time', $token_info['expiry_time']);
+ $smarty->assign('mail_expiry_time_formatted', $expiry_time_formatted);
+ $smarty->assign('mail_apass', $actpass);
$smarty->assign('mail_ip', $tikilib->get_ip_address());
$mail_data = sprintf($smarty->fetchLang($languageEmail, 'mail/password_reminder_subject.tpl'), $_SERVER["SERVER_NAME"]);
$mail = new TikiMail($name);
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/a88d01ebee32a2eeccb766615b0a4fb49379e319
--
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/a88d01ebee32a2eeccb766615b0a4fb49379e319
You're receiving this email because of your account on gitlab.com.
_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs