Secure password reset functionality
[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.
[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.