Files
pics/models/Authentication.php
T
yorick aca44ddd5c Fix (security): don't accept empty reset keys for users with no password reset pending
Currently, it's possible to reset passwords for users that do not have any password reset by providing an empty key, which would match '' or NULL.
2026-10-23 13:12:17 +02:00

174 lines
4.0 KiB
PHP

<?php
/*****************************************************************************
* Authentication.php
* Contains key class Authentication.
*
* Kabuki CMS (C) 2013-2015, Aaron van Geffen
*****************************************************************************/
/**
* Authentication class, containing various static functions used for account verification
* and session management.
*/
class Authentication
{
const DEFAULT_RESET_TIMEOUT = 30;
/**
* Checks a password for a given username against the database.
*/
public static function checkPassword($emailaddress, $password)
{
// Retrieve password hash for user matching the provided emailaddress.
$password_hash = Registry::get('db')->queryValue('
SELECT password_hash
FROM users
WHERE emailaddress = :emailaddress',
[
'emailaddress' => $emailaddress,
]);
// If there's no hash, the user likely does not exist.
if (!$password_hash)
return false;
return password_verify($password, $password_hash);
}
public static function checkResetKey($id_user, $reset_key)
{
// Users without a pending reset have a blank key, which must never match.
if (!is_string($reset_key) || $reset_key === '')
return false;
$key = Registry::get('db')->queryValue('
SELECT reset_key
FROM users
WHERE id_user = :id',
[
'id' => $id_user,
]);
if (!is_string($key) || $key === '')
return false;
return hash_equals($key, $reset_key);
}
/**
* Computes a password hash.
*/
public static function computeHash($password)
{
$hash = password_hash($password, PASSWORD_DEFAULT);
if (!$hash)
throw new Exception('Hash creation failed!');
return $hash;
}
public static function consumeResetKey($id_user)
{
return Registry::get('db')->query('
UPDATE users
SET reset_key = NULL,
reset_blocked_until = NULL
WHERE id_user = :id_user',
['id_user' => $id_user]);
}
public static function getResetTimeOut($id_user)
{
$resetTime = Registry::get('db')->queryValue('
SELECT reset_blocked_until
FROM users
WHERE id_user = :id_user',
['id_user' => $id_user]);
return max(0, $resetTime - time());
}
/**
* Verifies whether the user is currently logged in.
*/
public static function isLoggedIn()
{
if (!isset($_SESSION['user_id']))
return false;
try
{
$exists = Member::fromId($_SESSION['user_id']);
return true;
}
catch (NotFoundException $e)
{
return false;
}
}
/**
* Generates a new activation key.
*/
public static function newActivationKey()
{
$alpha = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
$string = '';
for ($i = 0; $i < 16; $i++)
$string .= $alpha[mt_rand(0, strlen($alpha) - 1)];
return $string;
}
public static function setResetKey($id_user)
{
return Registry::get('db')->query('
UPDATE users
SET reset_key = :key,
reset_blocked_until = UNIX_TIMESTAMP() + ' . static::DEFAULT_RESET_TIMEOUT . '
WHERE id_user = :id',
[
'id' => $id_user,
'key' => self::newActivationKey(),
]);
}
/**
* Resets a password for a certain user.
*/
public static function updatePassword($id_user, $hash)
{
return Registry::get('db')->query('
UPDATE users
SET
password_hash = :hash,
reset_key = :blank
WHERE id_user = :id_user',
[
'id_user' => $id_user,
'hash' => $hash,
'blank' => '',
]);
}
public static function updateResetTimeOut($id_user)
{
$currentResetTimeOut = static::getResetTimeOut($id_user);
// New timeout: between 30 seconds, double the current timeout, and a full day
$newResetTimeOut = min(max(static::DEFAULT_RESET_TIMEOUT, $currentResetTimeOut * 2), 60 * 60 * 24);
$success = Registry::get('db')->query('
UPDATE users
SET reset_blocked_until = :new_time_out
WHERE id_user = :id_user',
[
'id_user' => $id_user,
'new_time_out' => time() + $newResetTimeOut,
]);
if (!$success)
throw new UnexpectedValueException('Could not set password reset timeout!');
return $newResetTimeOut;
}
}