Validate that sensitive emails are sent to a single recipient
What does this MR do and why?
Validate that sensitive emails are sent to a single recipient
This is a defense-in-depth measure. It introduces a "time of use" check to validate that, when appropriate, an email is only being sent to a single email address. This should already be the case but, in the event a bug is introduced further up the stack in the future, this should prevent potential security vulnerabilities1.
This check is performed for:
- All Devise emails2:
confirmation_instructionsreset_password_instructionsunlock_instructionsemail_changedpassword_change
- GitLab's extra emails in
DeviseMailer:password_change_by_adminuser_admin_approvalreset_password_instructionsemail_changed
- Identity Verification3 emails:
verification_instructions_emailconfirmation_instructions_email
None of these emails should be sent to more than one person, or have other recipients in the CC or BCC fields. An exception will be raised if this occurs.
As many of these emails are sent in a delayed manner, the exceptions will be handled by ActiveJob.
References:
- Account Takeover via Password Reset without use... (#436084 - closed)
- GitLab's
DeviseMaileris set as Devise's mailer inconfig/initializers/8_devise.rb:22 - https://docs.gitlab.com/ee/security/identity_verification.html
- https://gitlab.com/gitlab-org/gitlab/-/issues/442831+
Will this break things?
I'm pretty confident it wont:
- None of the methods above involve cc or bccing anyone
- All of the use of devise mailers pass either a user (which Devise automagically gets the
user.emailout of), or passesto: user.email.
Click to expand grep
% rg "cc:" -g '!spec' -g '*.rb'
lib/gitlab/email/handler/service_desk_handler.rb
127: cc: mail.cc
ee/app/models/concerns/identity_verifiable.rb
144: # { email: true, phone: false, cc: false }, then prerequisite methods state
ee/app/mailers/ci_minutes_usage_mailer.rb
12: bcc: recipients,
22: bcc: recipients,
ee/app/mailers/emails/namespace_storage_usage_mailer.rb
23: bcc: recipients,
40: bcc: recipients,
ee/app/mailers/license_mailer.rb
14: bcc: recipients,
app/controllers/projects/service_desk_controller.rb
52: add_external_participants_from_cc: service_desk_settings&.add_external_participants_from_cc
% rg "DeviseMailer" -g '!spec' -g '*.rb'
config/initializers/8_devise.rb
22: config.mailer = "DeviseMailer"
app/services/users/approve_service.rb
18: DeviseMailer.user_admin_approval(user).deliver_later
app/mailers/previews/devise_mailer_preview.rb
3:class DeviseMailerPreview < ActionMailer::Preview
5: DeviseMailer.confirmation_instructions(unsaved_user, 'faketoken', {})
12: DeviseMailer.confirmation_instructions(user, 'faketoken', {})
16: DeviseMailer.reset_password_instructions(unsaved_user, 'faketoken', {})
20: DeviseMailer.unlock_instructions(unsaved_user, 'faketoken', {})
24: DeviseMailer.password_change(unsaved_user, {})
28: DeviseMailer.user_admin_approval(unsaved_user, {})
32: DeviseMailer.email_changed(unsaved_user, {})
app/mailers/devise_mailer.rb
3:class DeviseMailer < Devise::Mailer
% rg 'confirmation_instructions|reset_password_instructions|unlock_instructions|email_changed|password_change|password_change_by_admin|user_admin_approval|reset_password_instructions|email_changed|verification_instructions_email|confirmation_instructions_email' -g '!spec' -g '*.rb'
vendor/gems/attr_encrypted/test/active_record_test.rb
188: refute person.email_changed?
190: refute person.email_changed?
192: assert person.email_changed?
194: assert person.email_changed?
app/services/users/approve_service.rb
15: # Please see Devise's implementation of `resend_confirmation_instructions` for detail.
16: user.resend_confirmation_instructions
18: DeviseMailer.user_admin_approval(user).deliver_later
app/services/users/update_service.rb
64: return unless @user.email_changed?
71: return unless @user.email_changed?
app/services/emails/confirm_service.rb
6: email.resend_confirmation_instructions
app/controllers/registrations_controller.rb
47: send_custom_confirmation_instructions
320: def send_custom_confirmation_instructions
app/controllers/concerns/authenticates_with_two_factor.rb
45: return handle_changed_user(user) if user_password_changed?(user)
154: def user_password_changed?(user)
app/controllers/concerns/verifies_with_email.rb
99: send_verification_instructions_email(user, raw_token)
102: def send_verification_instructions_email(user, token)
106: Notify.verification_instructions_email(email, token: token).deliver_later
app/controllers/confirmations_controller.rb
20: def after_resending_confirmation_instructions_path_for(resource)
app/controllers/profiles/emails_controller.rb
4: before_action :find_email, only: [:destroy, :resend_confirmation_instructions]
8: only: [:resend_confirmation_instructions]
36: def resend_confirmation_instructions
app/controllers/user_settings/passwords_controller.rb
60: current_user.send_reset_password_instructions
app/controllers/passwords_controller.rb
65: redirect_to after_sending_reset_password_instructions_path_for(resource_name),
app/models/user.rb
326: validate :check_password_weakness, if: :encrypted_password_changed?
331: validate :unique_email, if: :email_changed?
332: validate :notification_email_verified, if: :notification_email_changed?
333: validate :public_email_verified, if: :public_email_changed?
334: validate :commit_email_verified, if: :commit_email_changed?
335: validate :email_allowed_by_restrictions?, if: ->(user) { user.new_record? ? !user.created_by_id : user.email_changed? }
354: before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
355: before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
1589: !email_changed? || new_record?
app/models/concerns/admin_changed_password_notifier.rb
40: skip_password_change_notification! # skip sending the default Devise 'password changed' notification
47: send_devise_notification(:password_change_by_admin)
55: self.class.send_password_change_notification && saved_change_to_encrypted_password? &&
app/models/concerns/recoverable_by_any_email.rb
10: def send_reset_password_instructions(attributes = {})
19: recoverable.send_reset_password_instructions(to: email.email)
24: def send_reset_password_instructions(opts = {})
27: send_reset_password_instructions_notification(token, opts)
39: def send_reset_password_instructions_notification(token, opts = {})
40: send_devise_notification(:reset_password_instructions, token, opts)
app/models/notification_setting.rb
17: validate :notification_email_verified, if: :notification_email_changed?
app/models/integrations/jira.rb
676: api_url_changed? || url_changed? || username_changed? || password_changed?
app/models/email.rb
11: validate :unique_email, if: ->(email) { email.email_changed? }
app/mailers/previews/notify_preview.rb
330: def verification_instructions_email
331: Notify.verification_instructions_email(user.email, token: '123456').message
app/mailers/previews/devise_mailer_preview.rb
4: def confirmation_instructions_for_signup
5: DeviseMailer.confirmation_instructions(unsaved_user, 'faketoken', {})
8: def confirmation_instructions_for_new_email
12: DeviseMailer.confirmation_instructions(user, 'faketoken', {})
15: def reset_password_instructions
16: DeviseMailer.reset_password_instructions(unsaved_user, 'faketoken', {})
19: def unlock_instructions
20: DeviseMailer.unlock_instructions(unsaved_user, 'faketoken', {})
23: def password_change
24: DeviseMailer.password_change(unsaved_user, {})
27: def user_admin_approval
28: DeviseMailer.user_admin_approval(unsaved_user, {})
31: def email_changed
32: DeviseMailer.email_changed(unsaved_user, {})
app/mailers/devise_mailer.rb
21: def password_change_by_admin(record, opts = {})
22: devise_mail(record, :password_change_by_admin, opts)
25: def user_admin_approval(record, opts = {})
26: devise_mail(record, :user_admin_approval, opts)
29: def reset_password_instructions(record, token, opts = {})
34: def email_changed(record, opts = {})
36: devise_mail(record, :email_changed_gitlab_com, opts)
38: devise_mail(record, :email_changed, opts)
config/initializers/8_devise.rb
128: config.send_password_change_notification = true
131: config.send_email_changed_notification = true
config/routes/profile.rb
61: put :resend_confirmation_instructions
app/mailers/emails/identity_verification.rb
7: def verification_instructions_email(email, token:)
ee/app/services/users/email_verification/send_custom_confirmation_instructions_service.rb
40: ::Notify.confirmation_instructions_email(user.email, token: token).deliver_later
ee/app/controllers/users/identity_verification_controller.rb
298: Notify.confirmation_instructions_email(@user.email, token: token).deliver_later
ee/app/controllers/ee/confirmations_controller.rb
18: audit_changes(:email, as: 'email address', model: resource, event_type: 'user_email_changed_and_user_signed_in')
ee/app/controllers/ee/registrations_controller.rb
63: custom_confirmation_instructions_service.set_token(save: false)
78: override :send_custom_confirmation_instructions
79: def send_custom_confirmation_instructions
82: custom_confirmation_instructions_service.send_instructions
85: def custom_confirmation_instructions_service
88: strong_memoize_attr :custom_confirmation_instructions_service
ee/app/mailers/ee/preview/notify_preview.rb
83: def confirmation_instructions_email
84: ::Notify.confirmation_instructions_email(user.email, token: '123456').message
ee/app/mailers/ee/emails/identity_verification.rb
8: def confirmation_instructions_email(email, token:)
ee/app/models/ee/user.rb
46: user.email_changed? && user.enterprise_user? && !user.skip_enterprise_user_email_change_restrictions?
51: after_update :email_changed_hook, if: :saved_change_to_email?
684: def send_confirmation_instructions
823: def email_changed_hook
MR acceptance checklist
Please evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.
Screenshots or screen recordings
Screenshots are required for UI changes, and strongly recommended for all other merge requests.
| Before | After |
|---|---|
How to set up and validate locally
Numbered steps to set up and validate the change are strongly suggested.
Related to #442831