CVE-2022-31025 (Ruby, published 2022-06-07T15:15Z)
Description: value: Discourse is an open source platform for community discussion. Prior to version 2.8.4 on the `stable` branch and 2.9.0beta5 on the `beta` and `tests-passed` branches, inviting users on sites that use single sign-on could bypass the `must_approve_users` check and invites by staff are always approved automatically. The issue is patched in Discourse version 2.8.4 on the `stable` branch and version `2.9.0.beta5` on the `beta` and `tests-passed` branches. As a workaround, disable invites or increase `min_trust_level_to_allow_invite` to reduce the attack surface to more trusted users.
Severity: MEDIUM (CVSS v3 base score: 5.3)
Weakness: NVD-CWE-Other \u2014 Other
NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
Repository: https://github.com/discourse/discourse
Files changed:
app/controllers/session_controller.rb
app/models/invite_redeemer.rb
app/models/reviewable_user.rb
spec/models/invite_redeemer_spec.rb
spec/models/invite_spec.rb
spec/requests/session_controller_spec.rb
Commit message: SECURITY: Remove auto approval when redeeming an invite (#16974)

This security fix affects sites which have `SiteSetting.must_approve_users`
enabled. There are intentional and unintentional cases where invited
users can be auto approved and are deemed to have skipped the staff approval process.
Instead of trying to reason about when auto-approval should happen, we have decided that
enabling the `must_approve_users` setting going forward will just mean that all new users
must be explicitly approved by a staff user in the review queue. The only case where users are auto
approved is when the `auto_approve_email_domains` site setting is used.

Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>

Vulnerable code:
        # users logging in via SSO using an invite do not need to be approved,
        # they are already pre-approved because they have been invited
        if SiteSetting.must_approve_users? && !user.approved? && invite.blank?
    if !SiteSetting.must_approve_users? ||
        (SiteSetting.must_approve_users? && invite.invited_by.staff?) ||
        EmailValidator.can_auto_approve_user?(user.email)
      ReviewableUser.set_approved_fields!(user, invite.invited_by)
    approve_account_if_needed
  def approve_account_if_needed
    if invited_user.present? && reviewable_user = ReviewableUser.find_by(target: invited_user, status: Reviewable.statuses[:pending])
      reviewable_user.perform(
        invite.invited_by,
        :approve_user,
        send_email: false,
        approved_by_invite: true
      )
    end
  end

    if guardian.can_approve?(target) || args[:approved_by_invite]
  describe '#create_user_from_invite' do
      expect(user.approved).to eq(true)
      expect(user.approved).to eq(true)
      expect(user.approved).to eq(true)
      expect(user.approved).to eq(true)
      expect(user.approved).to eq(true)
      expect(user.active).to eq(false)
    end

    it "does not automatically approve users if must_approve_users is true" do
      SiteSetting.must_approve_users = true

      invite = Fabricate(:invite, email: 'test@example.com')
      user = InviteRedeemer.create_user_from_invite(invite: invite, email: invite.email, username: 'test')
    end

    it "approves user if invited by staff" do
      SiteSetting.must_approve_users = true

      invite = Fabricate(:invite, email: 'test@example.com', invited_by: Fabricate(:admin))
      user = InviteRedeemer.create_user_from_invite(invite: invite, email: invite.email, username: 'test')
      expect(user.approved).to eq(true)
    it "should redeem the invite if invited by staff" do
      SiteSetting.must_approve_users = true
      inviter = invite.invited_by
      inviter.admin = true
      user = invite_redeemer.redeem
      invite.reload
      expect(user.name).to eq(name)
      expect(user.username).to eq(username)
      expect(user.invited_by).to eq(inviter)
      expect(inviter.notifications.count).to eq(1)
      expect(user.approved).to eq(true)
    end
    it "should redeem the invite if invited by non staff but not approve" do
      SiteSetting.must_approve_users = true
      inviter = invite.invited_by
      user = invite_redeemer.redeem
      expect(user.name).to eq(name)
      expect(user.username).to eq(username)
      expect(user.invited_by).to eq(inviter)
      expect(inviter.notifications.count).to eq(1)
      expect(user.approved).to eq(false)
      expect(user.approved).to eq(true)
    end

    it "should redeem the invite if invited by non staff and approve if email in auto_approve_email_domains setting" do
      SiteSetting.must_approve_users = true
      SiteSetting.auto_approve_email_domains = "example.com"
      user = invite_redeemer.redeem

      expect(user.name).to eq(name)
      expect(user.username).to eq(username)
      expect(user.approved).to eq(true)
      expect(user.approved).to eq(true)
    context "ReviewableUser" do
      it "approves pending record" do
        reviewable = ReviewableUser.needs_review!(target: Fabricate(:user, email: invite.email), created_by: invite.invited_by)
        reviewable.status = Reviewable.statuses[:pending]
        reviewable.save!
        invite_redeemer.redeem

        reviewable.reload
        expect(reviewable.status).to eq(Reviewable.statuses[:approved])
      end

      it "does not raise error if record is not pending" do
        reviewable = ReviewableUser.needs_review!(target: Fabricate(:user, email: invite.email), created_by: invite.invited_by)
        reviewable.status = Reviewable.statuses[:ignored]
        reviewable.save!
        invite_redeemer.redeem

        reviewable.reload
        expect(reviewable.status).to eq(Reviewable.statuses[:ignored])
      end
    end

    it 'activates user when must_approve_users? is enabled' do
      SiteSetting.must_approve_users = true
      invite.invited_by = Fabricate(:admin)

      user = invite.redeem
      expect(user.approved?).to eq(true)
    end

      it "allows you to create an account and redeems the invite successfully even if must_approve_users is enabled" do
        expect(response.status).to eq(302)
        expect(response).to redirect_to("/")

Fixed code:
        if SiteSetting.must_approve_users? && !user.approved?
          if invite.present? && user.invited_user.blank?
            redeem_invitation(invite, sso)
          end

    if SiteSetting.must_approve_users? && EmailValidator.can_auto_approve_user?(user.email)
      ReviewableUser.set_approved_fields!(user, Discourse.system_user)
    if guardian.can_approve?(target)
  describe '.create_user_from_invite' do
      expect(user.approved).to eq(false)
      expect(user.approved).to eq(false)
      expect(user.approved).to eq(false)
      expect(user.approved).to eq(false)
      expect(user.active).to eq(false)
    context "when must_approve_users setting is enabled" do
      before do
        SiteSetting.must_approve_users = true
      end
      it "should redeem an invite but not approve the user when invite is created by a staff user" do
        inviter = invite.invited_by
        inviter.update!(admin: true)
        user = invite_redeemer.redeem
        expect(user.name).to eq(name)
        expect(user.username).to eq(username)
        expect(user.invited_by).to eq(inviter)
        expect(user.approved).to eq(false)
        expect(inviter.notifications.count).to eq(1)
      end

      it "should redeem the invite but not approve the user when invite is created by a regular user" do
        inviter = invite.invited_by
        user = invite_redeemer.redeem

        expect(user.name).to eq(name)
        expect(user.username).to eq(username)
        expect(user.invited_by).to eq(inviter)
        expect(user.approved).to eq(false)

        expect(inviter.notifications.count).to eq(1)
      end

      it "should redeem the invite and approve the user when user email is in auto_approve_email_domains setting" do
        SiteSetting.auto_approve_email_domains = "example.com"
        user = invite_redeemer.redeem

        expect(user.name).to eq(name)
        expect(user.username).to eq(username)
        expect(user.approved).to eq(true)
        expect(user.approved_by).to eq(Discourse.system_user)
      end
      expect(user.approved).to eq(false)
      expect(user.approved).to eq(false)
      it "creates the user account and redeems the invite but does not approve the user if must_approve_users is enabled" do
        expect(response.status).to eq(403)
        expect(response.parsed_body).to include(I18n.t("discourse_connect.account_not_approved"))
        expect(user.approved).to eq(false)