From 2355cda40d4fa5fd956d7e090d2cd2e97570a509 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 10 Jan 2024 13:57:37 +0000 Subject: Add latest changes from gitlab-org/security/gitlab@16-4-stable-ee --- app/models/concerns/recoverable_by_any_email.rb | 25 ++-- app/views/devise/passwords/new.html.haml | 2 +- locale/gitlab.pot | 6 +- spec/controllers/passwords_controller_spec.rb | 131 ++++++++++++++++++++- spec/mailers/devise_mailer_spec.rb | 8 +- .../concerns/recoverable_by_any_email_spec.rb | 131 +++++++++++++++------ spec/support/helpers/email_helpers.rb | 12 ++ 7 files changed, 256 insertions(+), 59 deletions(-) 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/locale/gitlab.pot b/locale/gitlab.pot index 549557486d9..c21a095dea9 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -39896,15 +39896,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/mailers/devise_mailer_spec.rb b/spec/mailers/devise_mailer_spec.rb index 0a6d38996b7..af3a9457527 100644 --- a/spec/mailers/devise_mailer_spec.rb +++ b/spec/mailers/devise_mailer_spec.rb @@ -103,10 +103,10 @@ RSpec.describe DeviseMailer, feature_category: :user_management 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, feature_category: :user_management 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) -- cgit v1.2.3 From 80a6dc9a2db02c8061f6e68321c1a06a0d912427 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Wed, 10 Jan 2024 21:22:48 +0000 Subject: Add latest changes from gitlab-org/security/gitlab@16-4-stable-ee --- .gitlab/ci/global.gitlab-ci.yml | 2 +- .gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml | 2 +- .gitlab/ci/package-and-test/main.gitlab-ci.yml | 2 +- .gitlab/ci/review-apps/qa.gitlab-ci.yml | 2 +- .gitlab/ci/test-on-gdk/main.gitlab-ci.yml | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 51e23dce320..8611adf99f7 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -459,7 +459,7 @@ extends: .use-pg14 services: - !reference [.db-services-with-auto-explain, services] - - name: clickhouse/clickhouse-server:23-alpine + - name: clickhouse/clickhouse-server:23.11.3.23-alpine alias: clickhouse variables: CLICKHOUSE_USER: clickhouse diff --git a/.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml b/.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml index bb0de4a79e2..c657e43cb22 100644 --- a/.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test-nightly/main.gitlab-ci.yml @@ -2,7 +2,7 @@ include: - local: .gitlab/ci/qa-common/main.gitlab-ci.yml - local: .gitlab/ci/qa-common/rules.gitlab-ci.yml - local: .gitlab/ci/qa-common/variables.gitlab-ci.yml - - component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@7.3.0" + - component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@8.0.0" inputs: job_name: "e2e-test-report" job_stage: "report" diff --git a/.gitlab/ci/package-and-test/main.gitlab-ci.yml b/.gitlab/ci/package-and-test/main.gitlab-ci.yml index c616fe3de82..df48dbd9ef1 100644 --- a/.gitlab/ci/package-and-test/main.gitlab-ci.yml +++ b/.gitlab/ci/package-and-test/main.gitlab-ci.yml @@ -4,7 +4,7 @@ include: - local: .gitlab/ci/qa-common/main.gitlab-ci.yml - local: .gitlab/ci/qa-common/rules.gitlab-ci.yml - local: .gitlab/ci/qa-common/variables.gitlab-ci.yml - - component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@7.3.1" + - component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@8.0.0" inputs: job_name: "e2e-test-report" job_stage: "report" diff --git a/.gitlab/ci/review-apps/qa.gitlab-ci.yml b/.gitlab/ci/review-apps/qa.gitlab-ci.yml index a9ab031e115..f3d7d2c7a23 100644 --- a/.gitlab/ci/review-apps/qa.gitlab-ci.yml +++ b/.gitlab/ci/review-apps/qa.gitlab-ci.yml @@ -1,7 +1,7 @@ include: - local: .gitlab/ci/qa-common/main.gitlab-ci.yml - template: Verify/Browser-Performance.gitlab-ci.yml - - component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@7.3.0" + - component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@8.0.0" inputs: job_name: "e2e-test-report" job_stage: "post-qa" diff --git a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml index 9e179fec458..c65a9d4a0f0 100644 --- a/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml +++ b/.gitlab/ci/test-on-gdk/main.gitlab-ci.yml @@ -3,7 +3,7 @@ include: - local: .gitlab/ci/qa-common/main.gitlab-ci.yml - local: .gitlab/ci/qa-common/rules.gitlab-ci.yml - local: .gitlab/ci/qa-common/variables.gitlab-ci.yml - - component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@7.3.0" + - component: "gitlab.com/gitlab-org/quality/pipeline-common/allure-report@8.0.0" inputs: job_name: "e2e-test-report" job_stage: "report" -- cgit v1.2.3 From 96dd2b21ce4998e19ad011a8db74b9742032a3ca Mon Sep 17 00:00:00 2001 From: GitLab Release Tools Bot Date: Thu, 11 Jan 2024 00:34:17 +0000 Subject: Update VERSION files [ci skip] --- VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/VERSION b/VERSION index efec0ed1dec..f2348c07bf0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -16.4.4 \ No newline at end of file +16.4.5 \ No newline at end of file -- cgit v1.2.3 From 02b2cc5f47bd17fee6aefe63d3a97aa8d4060856 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 11 Jan 2024 00:41:34 +0000 Subject: Add latest changes from gitlab-org/security/gitlab@16-4-stable-ee --- CHANGELOG.md | 7 +++++++ GITALY_SERVER_VERSION | 2 +- GITLAB_PAGES_VERSION | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb373a31b2a..3c7c4750e5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ documentation](doc/development/changelog.md) for instructions on adding your own entry. +## 16.4.5 (2024-01-11) + +### Security (2 changes) + +- [Fix clickouse-server version in CI](gitlab-org/security/gitlab@71878185c35a1025d00eebc6dea54688fb93b585) ([merge request](gitlab-org/security/gitlab!3808)) +- [User password reset accepts multiple email addresses](gitlab-org/security/gitlab@394e91005d4a7b3df36be077aecf8a36acdee0f3) ([merge request](gitlab-org/security/gitlab!3795)) + ## 16.4.4 (2023-12-13) ### Security (8 changes) diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index efec0ed1dec..f2348c07bf0 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -16.4.4 \ No newline at end of file +16.4.5 \ No newline at end of file diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION index efec0ed1dec..f2348c07bf0 100644 --- a/GITLAB_PAGES_VERSION +++ b/GITLAB_PAGES_VERSION @@ -1 +1 @@ -16.4.4 \ No newline at end of file +16.4.5 \ No newline at end of file -- cgit v1.2.3