Chat's updates for security, db and workflow
Security and workflow regressions / regression (push) Has been cancelled

This commit is contained in:
2026-10-20 06:45:30 -04:00
parent 02223083f6
commit 1e63076123
116 changed files with 3252 additions and 2932 deletions
+22 -6
View File
@@ -61,6 +61,7 @@ class AuthService extends Base {
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
$stmt = $this->connect()->prepare('UPDATE users SET user_last_login = :now WHERE user_serial = :id');
$stmt->execute([':now' => $this->getTimestamp(), ':id' => $user['user_serial']]);
Audit::record($this->connect(),(int)$user['user_serial'],'logged in ('.$user['user_role'].')');
return true;
}
@@ -92,7 +93,7 @@ class AuthService extends Base {
if (!filter_var($email, FILTER_VALIDATE_EMAIL) || strlen($email) > 100)
throw new InvalidArgumentException('Enter a valid email address (up to 100 bytes).');
if (!preg_match('/\A[A-Za-z0-9_.-]{3,64}\z/', $name))
throw new InvalidArgumentException('Use 3–64 letters, numbers, dots, underscores or hyphens for your username.');
throw new InvalidArgumentException('Use 3 to 64 letters, numbers, dots, underscores or hyphens for your username.');
$error = self::passwordError($input['user_password'] ?? '', $input['user_password_confirm'] ?? '');
if ($error)
throw new InvalidArgumentException($error);
@@ -113,14 +114,14 @@ class AuthService extends Base {
}
/** Never modify the password when somebody merely requests an email. */
public function requestReset(string $identifier): void {
public function requestReset(string $identifier): bool {
$ipAllowed = $this->allowAttempt('reset-ip', $_SERVER['REMOTE_ADDR'] ?? '', 10, 3600);
$accountAllowed = $this->allowAttempt('reset-account', $identifier, 3, 3600);
if (!$ipAllowed || !$accountAllowed)
return;
return false;
$user = $this->findUser($identifier);
if (!$user || $user['user_status'] !== 'active')
return;
return false;
$baseUrl = rtrim((string) $this->getSettings()->ApplicationUrl, '/');
if (!filter_var($baseUrl, FILTER_VALIDATE_URL) || parse_url($baseUrl, PHP_URL_SCHEME) !== 'https'
|| parse_url($baseUrl, PHP_URL_QUERY) !== null || parse_url($baseUrl, PHP_URL_FRAGMENT) !== null
@@ -129,15 +130,27 @@ class AuthService extends Base {
}
$token = bin2hex(random_bytes(32));
$db = $this->connect();
$stmt = $db->prepare('INSERT INTO password_resets (token_hash, user_serial, expires_at) VALUES (:hash, :id, DATE_ADD(UTC_TIMESTAMP(), INTERVAL 30 MINUTE))');
$hash = hash('sha256', $token);
$stmt->execute([':hash' => $hash, ':id' => $user['user_serial']]);
$db->beginTransaction();
try {
// Share the user-row lock with account edits and password changes. A
// request resolved before such a change must not mint a token afterward.
$stmt=$db->prepare('SELECT user_email,user_status,user_auth_version FROM users WHERE user_serial=? FOR UPDATE');
$stmt->execute([$user['user_serial']]);$current=$stmt->fetch();
if(!$current || $current['user_status']!=='active' || $current['user_email']!==$user['user_email'] || (int)$current['user_auth_version']!==(int)$user['user_auth_version']){
$db->rollBack();return false;
}
$stmt=$db->prepare('INSERT INTO password_resets (token_hash,user_serial,expires_at) VALUES (?,?,DATE_ADD(UTC_TIMESTAMP(),INTERVAL 30 MINUTE))');
$stmt->execute([$hash,$user['user_serial']]);$db->commit();
}catch(Throwable $e){if($db->inTransaction())$db->rollBack();throw $e;}
$url = $baseUrl . '/main.php?Xaction=resetpassword&token=' . rawurlencode($token);
if (!$this->deliverResetEmail($user, $url)) {
$stmt = $db->prepare('DELETE FROM password_resets WHERE token_hash = :hash');
$stmt->execute([':hash' => $hash]);
throw new RuntimeException('Password reset email delivery failed.');
}
Audit::record($db,(int)$user['user_serial'],'requested password reset');
return true;
}
protected function deliverResetEmail(array $user, string $url): bool {
@@ -174,7 +187,9 @@ class AuthService extends Base {
return false;
}
$this->storePassword($db, (int) $id, $password);
$stmt=$db->prepare('UPDATE users SET user_email_verified_at=UTC_TIMESTAMP() WHERE user_serial=?');$stmt->execute([$id]);
$db->commit();
Audit::record($db,(int)$id,'reset password');
return true;
} catch (Throwable $e) {
if ($db->inTransaction())
@@ -199,6 +214,7 @@ class AuthService extends Base {
}
$this->storePassword($db, $id, $password);
$db->commit();
Audit::record($db,$id,'changed password');
return true;
} catch (Throwable $e) {
if ($db->inTransaction())