CVE-2022-3945: Improper Restriction of Excessive Authentication Attempts (CWE-307)
value: Improper Restriction of Excessive Authentication Attempts in GitHub repository kareadita/kavita prior to 0.6.0.3.
CWE description: The product does not implement sufficient measures to prevent multiple failed authentication attempts within a short time frame, making it more susceptible to brute force attacks.
CVSS v3 base score: 5.3
Published: 2022-11-11T12:15Z
Repository: https://github.com/kareadita/kavita
Language: C#
Files changed: API/Controllers/AccountController.cs
Commit date: 2022-10-31 06:07:55 -0700
Commit message: Security Patches (#1624)

* Fixed an issue where migrate-email could be called when an admin already existed

* When logging in, ensure there is no bias towards username or password when rejecting

* Cleaned up some messaging around anonymous apis to ensure there are no attack vectors.
Diff: diff --git a/API/Controllers/AccountController.cs b/API/Controllers/AccountController.cs
index b0d6c43b..09e211d5 100644
--- a/API/Controllers/AccountController.cs
+++ b/API/Controllers/AccountController.cs
@@ -184,11 +184,11 @@ public class AccountController : BaseApiController
     {
         var user = await _userManager.Users
             .Include(u => u.UserPreferences)
             .SingleOrDefaultAsync(x => x.NormalizedUserName == loginDto.Username.ToUpper());
 
-        if (user == null) return Unauthorized("Invalid username");
+        if (user == null) return Unauthorized("Your credentials are not correct");
 
         var result = await _signInManager
             .CheckPasswordSignInAsync(user, loginDto.Password, true);
 
         if (result.IsLockedOut)
@@ -196,11 +196,11 @@ public class AccountController : BaseApiController
             return Unauthorized("You've been locked out from too many authorization attempts. Please wait 10 minutes.");
         }
 
         if (!result.Succeeded)
         {
-            return Unauthorized(result.IsNotAllowed ? "You must confirm your email first" : "Your credentials are not correct.");
+            return Unauthorized(result.IsNotAllowed ? "You must confirm your email first" : "Your credentials are not correct");
         }
 
         // Update LastActive on account
         user.LastActive = DateTime.Now;
         user.UserPreferences ??= new AppUserPreferences
@@ -630,19 +630,25 @@ public class AccountController : BaseApiController
         }
 
         return BadRequest("There was an error setting up your account. Please check the logs");
     }
 
+    /// <summary>
+    /// Last step in authentication flow, confirms the email token for email
+    /// </summary>
+    /// <param name="dto"></param>
+    /// <returns></returns>
     [AllowAnonymous]
     [HttpPost("confirm-email")]
     public async Task<ActionResult<UserDto>> ConfirmEmail(ConfirmEmailDto dto)
     {
         var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
 
         if (user == null)
         {
-            return BadRequest("The email does not match the registered email");
+            _logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email);
+            return BadRequest("Invalid email confirmation");
         }
 
         // Validate Password and Username
         var validationErrors = new List<ApiException>();
         validationErrors.AddRange(await _accountService.ValidateUsername(dto.Username));
@@ -652,11 +658,15 @@ public class AccountController : BaseApiController
         {
             return BadRequest(validationErrors);
         }
 
 
-        if (!await ConfirmEmailToken(dto.Token, user)) return BadRequest("Invalid Email Token");
+        if (!await ConfirmEmailToken(dto.Token, user))
+        {
+            _logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token);
+            return BadRequest("Invalid email confirmation");
+        }
 
         user.UserName = dto.Username;
         user.ConfirmationToken = null;
         var errors = await _accountService.ChangeUserPassword(user, dto.Password);
         if (errors.Any())
@@ -692,15 +702,19 @@ public class AccountController : BaseApiController
     public async Task<ActionResult> ConfirmEmailUpdate(ConfirmEmailUpdateDto dto)
     {
         var user = await _unitOfWork.UserRepository.GetUserByConfirmationToken(dto.Token);
         if (user == null)
         {
-            return BadRequest("Invalid Email Token");
+            _logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email);
+            return BadRequest("Invalid email confirmation");
         }
 
-        if (!await ConfirmEmailToken(dto.Token, user)) return BadRequest("Invalid Email Token");
-
+        if (!await ConfirmEmailToken(dto.Token, user))
+        {
+            _logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token);
+            return BadRequest("Invalid email confirmation");
+        }
 
         _logger.LogInformation("User is updating email from {OldEmail} to {NewEmail}", user.Email, dto.Email);
         var result = await _userManager.SetEmailAsync(user, dto.Email);
         if (!result.Succeeded)
         {
@@ -726,16 +740,20 @@ public class AccountController : BaseApiController
         try
         {
             var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
             if (user == null)
             {
-                return BadRequest("Invalid Details");
+                return BadRequest("Invalid credentials");
             }
 
             var result = await _userManager.VerifyUserTokenAsync(user, TokenOptions.DefaultProvider,
                 "ResetPassword", dto.Token);
-            if (!result) return BadRequest("Unable to reset password, your email token is not correct.");
+            if (!result)
+            {
+                _logger.LogInformation("Unable to reset password, your email token is not correct: {@Dto}", dto);
+                return BadRequest("Invalid credentials");
+            }
 
             var errors = await _accountService.ChangeUserPassword(user, dto.Password);
             return errors.Any() ? BadRequest(errors) : Ok("Password updated");
         }
         catch (Exception ex)
@@ -799,13 +817,17 @@ public class AccountController : BaseApiController
     [AllowAnonymous]
     [HttpPost("confirm-migration-email")]
     public async Task<ActionResult<UserDto>> ConfirmMigrationEmail(ConfirmMigrationEmailDto dto)
     {
         var user = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);
-        if (user == null) return BadRequest("This email is not on system");
+        if (user == null) return BadRequest("Invalid credentials");
 
-        if (!await ConfirmEmailToken(dto.Token, user)) return BadRequest("Invalid Email Token");
+        if (!await ConfirmEmailToken(dto.Token, user))
+        {
+            _logger.LogInformation("confirm-migration-email email token is invalid");
+            return BadRequest("Invalid credentials");
+        }
 
         await _unitOfWork.CommitAsync();
 
         user = await _unitOfWork.UserRepository.GetUserByUsernameAsync(user.UserName,
             AppUserIncludes.UserPreferences);
@@ -863,10 +885,14 @@ public class AccountController : BaseApiController
     /// <returns></returns>
     [AllowAnonymous]
     [HttpPost("migrate-email")]
     public async Task<ActionResult<string>> MigrateEmail(MigrateUserEmailDto dto)
     {
+        // If there is an admin account already, return
+        var users = await _unitOfWork.UserRepository.GetAdminUsersAsync();
+        if (users.Any()) return BadRequest("Admin already exists");
+
         // Check if there is an existing invite
         var emailValidationErrors = await _accountService.ValidateEmail(dto.Email);
         if (emailValidationErrors.Any())
         {
             var invitedUser = await _unitOfWork.UserRepository.GetUserByEmailAsync(dto.Email);

Vulnerable code:         if (user == null) return Unauthorized("Invalid username");
            return Unauthorized(result.IsNotAllowed ? "You must confirm your email first" : "Your credentials are not correct.");
            return BadRequest("The email does not match the registered email");
        if (!await ConfirmEmailToken(dto.Token, user)) return BadRequest("Invalid Email Token");
            return BadRequest("Invalid Email Token");
        if (!await ConfirmEmailToken(dto.Token, user)) return BadRequest("Invalid Email Token");

                return BadRequest("Invalid Details");
            if (!result) return BadRequest("Unable to reset password, your email token is not correct.");
        if (user == null) return BadRequest("This email is not on system");
        if (!await ConfirmEmailToken(dto.Token, user)) return BadRequest("Invalid Email Token");
Fixed code:         if (user == null) return Unauthorized("Your credentials are not correct");
            return Unauthorized(result.IsNotAllowed ? "You must confirm your email first" : "Your credentials are not correct");
    /// <summary>
    /// Last step in authentication flow, confirms the email token for email
    /// </summary>
    /// <param name="dto"></param>
    /// <returns></returns>
            _logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email);
            return BadRequest("Invalid email confirmation");
        if (!await ConfirmEmailToken(dto.Token, user))
        {
            _logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token);
            return BadRequest("Invalid email confirmation");
        }
            _logger.LogInformation("confirm-email failed from invalid registered email: {Email}", dto.Email);
            return BadRequest("Invalid email confirmation");
        if (!await ConfirmEmailToken(dto.Token, user))
        {
            _logger.LogInformation("confirm-email failed from invalid token: {Token}", dto.Token);
            return BadRequest("Invalid email confirmation");
        }
                return BadRequest("Invalid credentials");
            if (!result)
            {
                _logger.LogInformation("Unable to reset password, your email token is not correct: {@Dto}", dto);
                return BadRequest("Invalid credentials");
            }
        if (user == null) return BadRequest("Invalid credentials");
        if (!await ConfirmEmailToken(dto.Token, user))
        {
            _logger.LogInformation("confirm-migration-email email token is invalid");
            return BadRequest("Invalid credentials");
        }
        // If there is an admin account already, return
        var users = await _unitOfWork.UserRepository.GetAdminUsersAsync();
        if (users.Any()) return BadRequest("Admin already exists");