Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Release Tools Bot <delivery-team+release-tools@gitlab.com>2024-01-12 00:06:49 +0300
committerGitLab Release Tools Bot <delivery-team+release-tools@gitlab.com>2024-01-12 00:06:49 +0300
commit24d1060c0ae7d0ba432271da98f4fa20ab6fd671 (patch)
tree5bcb9cd8370f814bd6f7cb38ebad5f56181e8142
parent74c15107a80459ce07e7b46a62e379a0495758dc (diff)
parentb06c2e5fedd97c4d7329596904260a617dc2d6ad (diff)
Merge remote-tracking branch 'dev/16-1-stable' into 16-1-stable16-1-stable
-rw-r--r--.gitlab/ci/preflight.gitlab-ci.yml2
-rw-r--r--CHANGELOG.md6
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--VERSION2
-rw-r--r--app/models/concerns/recoverable_by_any_email.rb25
-rw-r--r--app/views/devise/passwords/new.html.haml2
-rw-r--r--config/settings.rb3
-rw-r--r--locale/gitlab.pot6
-rw-r--r--spec/controllers/passwords_controller_spec.rb131
-rw-r--r--spec/fast_spec_helper.rb1
-rw-r--r--spec/mailers/devise_mailer_spec.rb8
-rw-r--r--spec/models/concerns/recoverable_by_any_email_spec.rb131
-rw-r--r--spec/support/helpers/email_helpers.rb12
14 files changed, 267 insertions, 66 deletions
diff --git a/.gitlab/ci/preflight.gitlab-ci.yml b/.gitlab/ci/preflight.gitlab-ci.yml
index 968402b2ea5..25735abadeb 100644
--- a/.gitlab/ci/preflight.gitlab-ci.yml
+++ b/.gitlab/ci/preflight.gitlab-ci.yml
@@ -47,7 +47,7 @@ rails-production-server-boot-puma-cng:
extends:
- .rails-production-server-boot
script:
- - curl --silent https://gitlab.com/gitlab-org/build/CNG/-/raw/master/gitlab-webservice/configuration/puma.rb > config/puma.rb
+ - curl --silent https://gitlab.com/gitlab-org/build/CNG/-/raw/16-1-stable/gitlab-webservice/configuration/puma.rb > config/puma.rb
- sed --in-place "s:/srv/gitlab:${PWD}:" config/puma.rb
- bundle exec puma --environment production --config config/puma.rb &
- sleep 40 # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/114124#note_1309506358
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a4eec682387..5d85be33a9d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 16.1.6 (2024-01-11)
+
+### Security (1 change)
+
+- [User password reset accepts multiple email addresses](gitlab-org/security/gitlab@44deb06e2c8e1c1b9424fc89dec4f60c8806711a) ([merge request](gitlab-org/security/gitlab!3798))
+
## 16.1.5 (2023-08-31)
### Fixed (1 change)
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index d4f38ed58a5..abb5dced28a 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-16.1.5 \ No newline at end of file
+16.1.6 \ No newline at end of file
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index d4f38ed58a5..abb5dced28a 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-16.1.5 \ No newline at end of file
+16.1.6 \ No newline at end of file
diff --git a/VERSION b/VERSION
index d4f38ed58a5..abb5dced28a 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-16.1.5 \ No newline at end of file
+16.1.6 \ No newline at end of file
diff --git a/app/models/concerns/recoverable_by_any_email.rb b/app/models/concerns/recoverable_by_any_email.rb
index c946e7e78c6..7bd908597c9 100644
--- a/app/models/concerns/recoverable_by_any_email.rb
+++ b/app/models/concerns/recoverable_by_any_email.rb
@@ -1,37 +1,34 @@
# frozen_string_literal: true
-# Concern that overrides the Devise methods
-# to send reset password instructions to any verified user email
+# Concern that overrides the Devise methods to allow reset password instructions
+# to be sent to any users' confirmed secondary emails.
+# See https://github.com/heartcombo/devise/blob/main/lib/devise/models/recoverable.rb
module RecoverableByAnyEmail
extend ActiveSupport::Concern
class_methods do
def send_reset_password_instructions(attributes = {})
- email = attributes.delete(:email)
- super unless email
+ return super unless attributes[:email]
- recoverable = by_email_with_errors(email)
- recoverable.send_reset_password_instructions(to: email) if recoverable&.persisted?
- recoverable
- end
+ email = Email.confirmed.find_by(email: attributes[:email].to_s)
+ return super unless email
- private
+ recoverable = email.user
- def by_email_with_errors(email)
- record = find_by_any_email(email, confirmed: true) || new
- record.errors.add(:email, :invalid) unless record.persisted?
- record
+ recoverable.send_reset_password_instructions(to: email.email)
+ recoverable
end
end
def send_reset_password_instructions(opts = {})
token = set_reset_password_token
+
send_reset_password_instructions_notification(token, opts)
token
end
- private
+ protected
def send_reset_password_instructions_notification(token, opts = {})
send_devise_notification(:reset_password_instructions, token, opts)
diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml
index 8e55977fe7a..227418e366d 100644
--- a/app/views/devise/passwords/new.html.haml
+++ b/app/views/devise/passwords/new.html.haml
@@ -7,7 +7,7 @@
= f.label :email, _('Email')
= f.email_field :email, class: "form-control gl-form-input", required: true, autocomplete: 'off', value: params[:user_email], autofocus: true, title: _('Please provide a valid email address.')
.form-text.text-muted
- = _('Requires a verified GitLab email address.')
+ = _('Requires your primary or verified secondary GitLab email address.')
- if recaptcha_enabled?
.gl-mb-5
diff --git a/config/settings.rb b/config/settings.rb
index c25531a3311..138ca168e64 100644
--- a/config/settings.rb
+++ b/config/settings.rb
@@ -3,9 +3,8 @@
require_relative '../lib/gitlab_settings'
file = ENV.fetch('GITLAB_CONFIG') { Rails.root.join('config/gitlab.yml') }
-section = ENV.fetch('GITLAB_ENV') { Rails.env }
-Settings = GitlabSettings.load(file, section) do
+Settings = GitlabSettings.load(file, Rails.env) do
def gitlab_on_standard_port?
on_standard_port?(gitlab)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index f9834b74482..229b2e43cf2 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -39004,15 +39004,15 @@ msgid_plural "Requires %{count} approvals from %{names}."
msgstr[0] ""
msgstr[1] ""
-msgid "Requires a verified GitLab email address."
-msgstr ""
-
msgid "Requires you to deploy or set up cloud-hosted Sentry."
msgstr ""
msgid "Requires your primary GitLab email address."
msgstr ""
+msgid "Requires your primary or verified secondary GitLab email address."
+msgstr ""
+
msgid "Resend"
msgstr ""
diff --git a/spec/controllers/passwords_controller_spec.rb b/spec/controllers/passwords_controller_spec.rb
index aad946acad4..cff84da7382 100644
--- a/spec/controllers/passwords_controller_spec.rb
+++ b/spec/controllers/passwords_controller_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe PasswordsController do
+RSpec.describe PasswordsController, feature_category: :system_access do
include DeviseHelpers
before do
@@ -109,8 +109,9 @@ RSpec.describe PasswordsController do
describe '#create' do
let(:user) { create(:user) }
+ let(:email) { user.email }
- subject(:perform_request) { post(:create, params: { user: { email: user.email } }) }
+ subject(:perform_request) { post(:create, params: { user: { email: email } }) }
context 'when reCAPTCHA is disabled' do
before do
@@ -161,5 +162,131 @@ RSpec.describe PasswordsController do
expect(flash[:notice]).to include 'If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes.'
end
end
+
+ context "sending 'Reset password instructions' email" do
+ include EmailHelpers
+
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_confirmed_primary_email) { user.email }
+ let_it_be(:user_confirmed_secondary_email) { create(:email, :confirmed, user: user, email: 'confirmed-secondary-email@example.com').email }
+ let_it_be(:user_unconfirmed_secondary_email) { create(:email, user: user, email: 'unconfirmed-secondary-email@example.com').email }
+ let_it_be(:unknown_email) { 'attacker@example.com' }
+ let_it_be(:invalid_email) { 'invalid_email' }
+ let_it_be(:sql_injection_email) { 'sql-injection-email@example.com OR 1=1' }
+ let_it_be(:another_user_confirmed_primary_email) { create(:user).email }
+ let_it_be(:another_user_unconfirmed_primary_email) { create(:user, :unconfirmed).email }
+
+ before do
+ reset_delivered_emails!
+
+ perform_request
+
+ perform_enqueued_jobs
+ end
+
+ context "when email param matches user's confirmed primary email" do
+ let(:email) { user_confirmed_primary_email }
+
+ it 'sends email to the primary email only' do
+ expect_only_one_email_to_be_sent(subject: 'Reset password instructions', to: [user_confirmed_primary_email])
+ end
+ end
+
+ context "when email param matches user's unconfirmed primary email" do
+ let(:email) { another_user_unconfirmed_primary_email }
+
+ # By default 'devise' gem allows password reset by unconfirmed primary email.
+ # When user account with unconfirmed primary email that means it is unconfirmed.
+ #
+ # Password reset by unconfirmed primary email is very helpful from
+ # security perspective. Example:
+ # Malicious person creates user account on GitLab with someone's email.
+ # If the email owner confirms the email for newly created account, the malicious person will be able
+ # to sign in into the account by password they provided during account signup.
+ # The malicious person could set up 2FA to the user account, after that
+ # te email owner would not able to get access to that user account even
+ # after performing password reset.
+ # To deal with that case safely the email owner should reset password
+ # for the user account first. That will make sure that after the user account
+ # is confirmed the malicious person is not be able to sign in with
+ # the password they provided during the account signup. Then email owner
+ # could sign into the account, they will see a prompt to confirm the account email
+ # to proceed. They can safely confirm the email and take over the account.
+ # That is one of the reasons why password reset by unconfirmed primary email should be allowed.
+ it 'sends email to the primary email only' do
+ expect_only_one_email_to_be_sent(subject: 'Reset password instructions', to: [another_user_unconfirmed_primary_email])
+ end
+ end
+
+ context "when email param matches user's confirmed secondary email" do
+ let(:email) { user_confirmed_secondary_email }
+
+ it 'sends email to the confirmed secondary email only' do
+ expect_only_one_email_to_be_sent(subject: 'Reset password instructions', to: [user_confirmed_secondary_email])
+ end
+ end
+
+ # While unconfirmed primary emails are linked with users accounts,
+ # unconfirmed secondary emails should not be linked with any users till they are confirmed
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/356665
+ #
+ # In https://gitlab.com/gitlab-org/gitlab/-/issues/367823, it is considerd
+ # to prevent reserving emails on Gitlab by unconfirmed secondary emails.
+ # As per this issue, there might be cases that there are multiple users
+ # with the same unconfirmed secondary emails. It would be impossible to identify for
+ # what user account password reset is requested if password reset were allowed
+ # by unconfirmed secondary emails.
+ # Also note that it is not possible to request email confirmation for
+ # unconfirmed secondary emails without having access to the user account.
+ context "when email param matches user's unconfirmed secondary email" do
+ let(:email) { user_unconfirmed_secondary_email }
+
+ it 'does not send email to anyone' do
+ should_not_email_anyone
+ end
+ end
+
+ context 'when email param is unknown email' do
+ let(:email) { unknown_email }
+
+ it 'does not send email to anyone' do
+ should_not_email_anyone
+ end
+ end
+
+ context 'when email param is invalid email' do
+ let(:email) { invalid_email }
+
+ it 'does not send email to anyone' do
+ should_not_email_anyone
+ end
+ end
+
+ context 'when email param with attempt to cause SQL injection' do
+ let(:email) { sql_injection_email }
+
+ it 'does not send email to anyone' do
+ should_not_email_anyone
+ end
+ end
+
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/436084
+ context 'when email param with multiple emails' do
+ let(:email) do
+ [
+ user_confirmed_primary_email,
+ user_confirmed_secondary_email,
+ user_unconfirmed_secondary_email,
+ unknown_email,
+ another_user_confirmed_primary_email,
+ another_user_unconfirmed_primary_email
+ ]
+ end
+
+ it 'does not send email to anyone' do
+ should_not_email_anyone
+ end
+ end
+ end
end
end
diff --git a/spec/fast_spec_helper.rb b/spec/fast_spec_helper.rb
index fcf0c43243f..1e5cf457493 100644
--- a/spec/fast_spec_helper.rb
+++ b/spec/fast_spec_helper.rb
@@ -8,7 +8,6 @@ end
require_relative '../config/bundler_setup'
-ENV['GITLAB_ENV'] = 'test'
ENV['IN_MEMORY_APPLICATION_SETTINGS'] = 'true'
require './spec/deprecation_warnings'
diff --git a/spec/mailers/devise_mailer_spec.rb b/spec/mailers/devise_mailer_spec.rb
index 171251f51ef..98a315a43a7 100644
--- a/spec/mailers/devise_mailer_spec.rb
+++ b/spec/mailers/devise_mailer_spec.rb
@@ -103,10 +103,10 @@ RSpec.describe DeviseMailer do
describe '#reset_password_instructions' do
let_it_be(:user) { create(:user) }
- let(:params) { {} }
+ let(:opts) { {} }
subject do
- described_class.reset_password_instructions(user, 'faketoken', params)
+ described_class.reset_password_instructions(user, 'faketoken', opts)
end
it_behaves_like 'an email sent from GitLab'
@@ -139,9 +139,9 @@ RSpec.describe DeviseMailer do
is_expected.to have_header 'X-Mailgun-Suppressions-Bypass', 'true'
end
- context 'with email in params' do
+ context 'with email in opts' do
let(:email) { 'example@example.com' }
- let(:params) { { to: email } }
+ let(:opts) { { to: email } }
it 'is sent to the specified email' do
is_expected.to deliver_to email
diff --git a/spec/models/concerns/recoverable_by_any_email_spec.rb b/spec/models/concerns/recoverable_by_any_email_spec.rb
index 1e701f145be..ba0bb99effb 100644
--- a/spec/models/concerns/recoverable_by_any_email_spec.rb
+++ b/spec/models/concerns/recoverable_by_any_email_spec.rb
@@ -4,79 +4,140 @@ require 'spec_helper'
RSpec.describe RecoverableByAnyEmail, feature_category: :system_access do
describe '.send_reset_password_instructions' do
- let_it_be(:user) { create(:user, email: 'test@example.com') }
- let_it_be(:verified_email) { create(:email, :confirmed, user: user) }
- let_it_be(:unverified_email) { create(:email, user: user) }
+ include EmailHelpers
subject(:send_reset_password_instructions) do
User.send_reset_password_instructions(email: email)
end
- shared_examples 'sends the password reset email' do
+ let_it_be(:user) { create(:user) }
+ let_it_be(:user_confirmed_primary_email) { user.email }
+
+ let_it_be(:user_confirmed_secondary_email) do
+ create(:email, :confirmed, user: user, email: 'confirmed-secondary-email@example.com').email
+ end
+
+ let_it_be(:user_unconfirmed_secondary_email) do
+ create(:email, user: user, email: 'unconfirmed-secondary-email@example.com').email
+ end
+
+ let_it_be(:unknown_email) { 'attacker@example.com' }
+ let_it_be(:invalid_email) { 'invalid_email' }
+ let_it_be(:sql_injection_email) { 'sql-injection-email@example.com OR 1=1' }
+
+ let_it_be(:another_user_confirmed_primary_email) { create(:user).email }
+
+ let_it_be(:another_user) { create(:user, :unconfirmed) }
+ let_it_be(:another_user_unconfirmed_primary_email) { another_user.email }
+
+ shared_examples "sends 'Reset password instructions' email" do
it 'finds the user' do
- expect(send_reset_password_instructions).to eq(user)
+ expect(send_reset_password_instructions).to eq(expected_user)
end
it 'sends the email' do
+ reset_delivered_emails!
+
expect { send_reset_password_instructions }.to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
+
+ perform_enqueued_jobs
+
+ expect_only_one_email_to_be_sent(subject: 'Reset password instructions', to: [email])
end
end
- shared_examples 'does not send the password reset email' do
+ shared_examples "does not send 'Reset password instructions' email" do
+ # If user is not found, returns a new user with errors.
+ # See https://github.com/heartcombo/devise/blob/main/lib/devise/models/recoverable.rb
it 'does not find the user' do
- expect(subject.id).to be_nil
- expect(subject.errors).not_to be_empty
+ expect(send_reset_password_instructions).to be_instance_of User
+ expect(send_reset_password_instructions).to be_new_record
+ expect(send_reset_password_instructions.errors).not_to be_empty
end
- it 'does not send any email' do
- subject
+ it 'does not send email to anyone' do
+ reset_delivered_emails!
+
+ expect { send_reset_password_instructions }
+ .not_to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
+
+ perform_enqueued_jobs
- expect { subject }.not_to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
+ should_not_email_anyone
end
end
- context 'with user primary email' do
- let(:email) { user.email }
+ context "when email param matches user's confirmed primary email" do
+ let(:expected_user) { user }
+ let(:email) { user_confirmed_primary_email }
- it_behaves_like 'sends the password reset email'
+ it_behaves_like "sends 'Reset password instructions' email"
end
- context 'with user verified email' do
- let(:email) { verified_email.email }
+ context "when email param matches user's unconfirmed primary email" do
+ let(:expected_user) { another_user }
+ let(:email) { another_user_unconfirmed_primary_email }
- it_behaves_like 'sends the password reset email'
+ it_behaves_like "sends 'Reset password instructions' email"
end
- context 'with user unverified email' do
- let(:email) { unverified_email.email }
+ context "when email param matches user's confirmed secondary email" do
+ let(:expected_user) { user }
+ let(:email) { user_confirmed_secondary_email }
- it_behaves_like 'does not send the password reset email'
+ it_behaves_like "sends 'Reset password instructions' email"
end
- end
- describe '#send_reset_password_instructions' do
- let_it_be(:user) { create(:user) }
- let_it_be(:opts) { { email: 'random@email.com' } }
- let_it_be(:token) { 'passwordresettoken' }
+ context "when email param matches user's unconfirmed secondary email" do
+ let(:email) { user_unconfirmed_secondary_email }
- before do
- allow(user).to receive(:set_reset_password_token).and_return(token)
+ it_behaves_like "does not send 'Reset password instructions' email"
end
- subject { user.send_reset_password_instructions(opts) }
+ context 'when email param is unknown email' do
+ let(:email) { unknown_email }
- it 'sends the email' do
- expect { subject }.to have_enqueued_mail(DeviseMailer, :reset_password_instructions)
+ it_behaves_like "does not send 'Reset password instructions' email"
end
- it 'calls send_reset_password_instructions_notification with correct arguments' do
- expect(user).to receive(:send_reset_password_instructions_notification).with(token, opts)
+ context 'when email param is invalid email' do
+ let(:email) { invalid_email }
- subject
+ it_behaves_like "does not send 'Reset password instructions' email"
end
- it 'returns the generated token' do
- expect(subject).to eq(token)
+ context 'when email param with attempt to cause SQL injection' do
+ let(:email) { sql_injection_email }
+
+ it_behaves_like "does not send 'Reset password instructions' email"
+ end
+
+ context 'when email param is nil' do
+ let(:email) { nil }
+
+ it_behaves_like "does not send 'Reset password instructions' email"
+ end
+
+ context 'when email param is empty string' do
+ let(:email) { '' }
+
+ it_behaves_like "does not send 'Reset password instructions' email"
+ end
+
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/436084
+ context 'when email param with multiple emails' do
+ let(:email) do
+ [
+ user_confirmed_primary_email,
+ user_confirmed_secondary_email,
+ user_unconfirmed_secondary_email,
+ unknown_email,
+ another_user_confirmed_primary_email,
+ another_user_unconfirmed_primary_email
+ ]
+ end
+
+ it_behaves_like "does not send 'Reset password instructions' email"
end
end
end
diff --git a/spec/support/helpers/email_helpers.rb b/spec/support/helpers/email_helpers.rb
index 57386233775..2a7d15f03c6 100644
--- a/spec/support/helpers/email_helpers.rb
+++ b/spec/support/helpers/email_helpers.rb
@@ -14,6 +14,18 @@ module EmailHelpers
ActiveJob::Base.queue_adapter.enqueued_jobs.clear
end
+ def expect_only_one_email_to_be_sent(subject:, to:)
+ count_of_sent_emails = ActionMailer::Base.deliveries.count
+ expect(count_of_sent_emails).to eq(1), "Expected only one email to be sent, but #{count_of_sent_emails} emails were sent instead"
+
+ return unless count_of_sent_emails == 1
+
+ message = ActionMailer::Base.deliveries.first
+
+ expect(message.subject).to eq(subject), "Expected '#{subject}' email to be sent, but '#{message.subject}' email was sent instead"
+ expect(message.to).to match_array(to), "Expected the email to be sent to #{to}, but it was sent to #{message.to} instead"
+ end
+
def should_only_email(*users, kind: :to)
recipients = email_recipients(kind: kind)