Account Takeover - Elixir

Account Takeover - Elixir

Need

To prevent unauthorized access and control over a user account.

Context

  • Usage of Elixir 1.12 for functional programming and building scalable applications
  • Usage of Plug for building composable web applications
  • Usage of Phoenix web framework for building web applications

Description

Non compliant code

        defmodule MyAppWeb.PasswordResetController do
  use MyAppWeb, :controller

  def create(conn, %{'email' => email}) do
    user = Accounts.get_user_by_email(email)
    Accounts.deliver_password_reset_instructions(user, conn)
    send_resp(conn, :ok, "")
  end
end

defmodule MyApp.Accounts do
  def deliver_password_reset_instructions(%User{} = user, conn) do
    url = MyAppWeb.Router.Helpers.password_reset_url(conn, :edit, user.reset_password_token)
    MyApp.Mailer.deliver_password_reset_instructions(user.email, url)
  end
end
        
        

The code implements a password reset functionality. However, it does not validate whether the requester is indeed the owner of the account. This allows an attacker to request a password reset for any account and receive the reset link, allowing them to takeover that account.

Steps

  • Implement verification mechanisms to ensure that the person requesting a password reset is indeed the owner of the account.
  • This could be done by sending a verification code to the user's registered email before proceeding with the password reset process.
  • Ensure that the password reset URL is only sent to the user's registered email and is not exposed anywhere else.

Compliant code

        defmodule MyAppWeb.PasswordResetController do
  use MyAppWeb, :controller

  def create(conn, %{'email' => email}) do
    user = Accounts.get_user_by_email(email)
    verification_code = Accounts.generate_verification_code(user)
    MyApp.Mailer.deliver_verification_code(user.email, verification_code)
    send_resp(conn, :ok, "")
  end

  def update(conn, %{'email' => email, 'verification_code' => verification_code, 'new_password' => new_password}) do
    user = Accounts.get_user_by_email(email)
    if Accounts.verify(user, verification_code) do
      Accounts.reset_password(user, new_password)
      send_resp(conn, :ok, "")
    else
      send_resp(conn, :bad_request, "")
    end
  end
end
        
        

The code now first sends a verification code to the user's registered email when a password reset is requested. The password reset process only proceeds if the correct verification code is provided, thereby validating that the requester is indeed the owner of the account.

References