logo

Account Takeover - Elixir


Need

To prevent unauthorized access and control over a user account.


Context

  1. Usage of Elixir 1.12 for functional programming and building scalable applications
  2. Usage of Plug for building composable web applications
  3. Usage of Phoenix web framework for building web applications

Description

Insecure Code Example

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

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

Secure Code Example

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

  • 417 - Account Takeover

  • Last updated

    2023/09/18