Prevention of unauthorized access through manipulated password reset links
class PasswordsController < ApplicationController
def create
user = User.find_by(email: params[:email])
if user
user.send_password_reset(params[:host])
else
redirect_to new_password_path, alert: "Invalid email address"
end
end
end
class User < ActiveRecord::Base
def send_password_reset(host)
generate_token(:password_reset_token)
self.password_reset_sent_at = Time.zone.now
save!
UserMailer.password_reset(self, host).deliver
end
end
class UserMailer < ActionMailer::Base
def password_reset(user, host)
@user = user
mail to: user.email, subject: "Password Reset", from: "no-reply@#{host}"
end
end
In the above code, the
PasswordsController
receives an email and a host as parameters. If a user with the provided email exists, it triggers the
send_password_reset
method in the User model. This method generates a password reset token, sets the time the password reset was sent, saves the user, and then sends an email to the user with the password reset link.
The
UserMailer
is responsible for sending the email. It sets the user and sends the email to the user's email address with a subject of "Password Reset" and a from address of "no-reply@#\{host\}".
The vulnerability lies in the fact that the host parameter can be manipulated by an attacker. Since the host parameter is used to generate the from address in the password reset email, an attacker can change the host to a domain under his control. This could lead to a phishing attack where the attacker sends a password reset email from a domain that looks similar to the legitimate domain, tricks the user into clicking the link and entering their new password on the attacker's site.
class PasswordsController < ApplicationController
def create
user = User.find_by(email: params[:email])
if user
user.send_password_reset(request.host)
else
redirect_to new_password_path, alert: "Invalid email address"
end
end
end
class User < ActiveRecord::Base
def send_password_reset(host)
generate_token(:password_reset_token)
self.password_reset_sent_at = Time.zone.now
save!
UserMailer.password_reset(self, host).deliver if valid_host?(host)
end
private
def valid_host?(host)
# Add your domain whitelist here
whitelist = ['yourdomain.com', 'anotherdomain.com']
whitelist.include?(host)
end
end
class UserMailer < ActionMailer::Base
def password_reset(user, host)
@user = user
mail to: user.email, subject: "Password Reset", from: "no-reply@#{host}"
end
end
The updated code includes a method
valid_host?
in the
User
model that checks if the provided host is included in a predefined whitelist of domains. This whitelist should include all domains that are authorized to send password reset emails. This way, even if an attacker tries to manipulate the host parameter, the password reset email will not be sent unless the host is included in the whitelist.
In the
PasswordsController
, the
request.host
is passed to the
send_password_reset
method instead of
params[:host]
. This ensures that the host is the one making the request, not a potentially manipulated host from the parameters.
These changes help to mitigate the risk of password reset poisoning by ensuring that only authorized domains can be used to send password reset emails. It's also recommended to use HTTPS for all password reset links, implement rate limiting or CAPTCHA to prevent abuse of the password reset functionality, and regularly update and patch the Ruby on Rails framework and its dependencies to address any security vulnerabilities.