Password reset poisoning - C-Sharp

Password reset poisoning - C-Sharp

Need

Secure password reset functionality

Context

  • Usage of C# 7.1 for advanced language features and improvements
  • Usage of Microsoft.AspNetCore.Mvc for building web applications with ASP.NET Core
  • Usage of Microsoft.AspNetCore.Authorization for implementing authorization and access control in ASP.NET Core applications
  • Usage of Microsoft.AspNetCore.Antiforgery for preventing cross-site request forgery attacks
  • Usage of Microsoft.AspNetCore.Identity for managing user authentication and authorization in ASP.NET Core applications
  • Usage of Microsoft.AspNetCore.Identity.UI for implementing user authentication and authorization in ASP.NET Core applications
  • Usage of Microsoft.AspNetCore.Mvc.RazorPages for building dynamic web pages with Razor syntax in ASP.NET Core
  • Usage of Microsoft.Extensions.DependencyInjection for dependency injection in .NET applications
  • Usage of Microsoft.Extensions.Logging for logging and tracing in .NET applications
  • Usage of Microsoft.AspNetCore.Mvc.Rendering for rendering HTML elements in ASP.NET Core MVC

Description

Non compliant code

        [HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task ForgotPassword(ForgotPasswordViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = await _userManager.FindByEmailAsync(model.Email);
        if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
        {
            return View("ForgotPasswordConfirmation");
        }

        var code = await _userManager.GeneratePasswordResetTokenAsync(user);
        var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
        await _emailSender.SendEmailAsync(model.Email, "Reset Password", $"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
        return View("ForgotPasswordConfirmation");
    }

    return View(model);
}
        
        

The above code is an action method in an ASP.NET Core controller that handles password reset requests. When a user forgets their password and requests a reset, this method is invoked.

The method first checks if the model state is valid. If it is, it finds the user by their email and checks if their email is confirmed. If the user is not found or their email is not confirmed, the method returns a "ForgotPasswordConfirmation" view.

If the user is found and their email is confirmed, the method generates a password reset token for the user. It then builds a callback URL for the password reset, which includes the user's ID and the reset token. The URL is created using the Url.Action method, which generates a fully qualified URL to an action method by using the specified action name, controller name, route values, and protocol to use.

The problem with this code is that it uses the HttpContext.Request.Scheme for the protocol of the URL. This means that the scheme (http or https) of the incoming request is used to generate the password reset link. If an attacker is able to manipulate the incoming request to use a scheme of a domain under their control, they can receive the password reset link instead of the intended user. This is known as Password Reset Poisoning.

Steps

  • Validate the domain of the password reset link to ensure it matches the expected domain.
  • Use a secure method to generate the password reset link, such as using a secure token or a one-time-use code.
  • Implement proper input validation to prevent any malicious input from being included in the password reset link.
  • Consider implementing additional security measures, such as rate limiting or CAPTCHA, to prevent abuse of the password reset functionality.
  • Regularly update and patch the application to ensure any known vulnerabilities are addressed.

Compliant code

        [HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task ForgotPassword(ForgotPasswordViewModel model)
{
    if (ModelState.IsValid)
    {
        var user = await _userManager.FindByEmailAsync(model.Email);
        if (user == null || !(await _userManager.IsEmailConfirmedAsync(user)))
        {
            return View("ForgotPasswordConfirmation");
        }

        var code = await _userManager.GeneratePasswordResetTokenAsync(user);
        var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme);
        
        // Validate the domain of the callbackUrl
        var callbackUri = new Uri(callbackUrl);
        if (callbackUri.Host != HttpContext.Request.Host.Value)
        {
            return BadRequest();
        }

        await _emailSender.SendEmailAsync(model.Email, "Reset Password", $"Please reset your password by clicking here: <a href='{callbackUrl}'>link</a>");
        return View("ForgotPasswordConfirmation");
    }

    return View(model);
}
        
        

The updated code now includes a check to validate the domain of the password reset link. This is done by creating a new Uri object from the callbackUrl and comparing its Host property with the Host value of the current HttpContext.Request. If the two do not match, the method returns a BadRequest result, preventing the password reset email from being sent.

This fix addresses the vulnerability by ensuring that the password reset link can only be generated for the expected domain, preventing an attacker from manipulating the request to change the domain to one under their control.

In addition to this fix, it is recommended to use a secure method to generate the password reset link, such as using a secure token or a one-time-use code, implement proper input validation to prevent any malicious input from being included in the password reset link, and consider implementing additional security measures, such as rate limiting or CAPTCHA, to prevent abuse of the password reset functionality. Regularly updating and patching the application to ensure any known vulnerabilities are addressed is also crucial.

References