CVE-2022-31025: Other (NVD-CWE-Other)
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.
CWE description: 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.
CVSS v3 base score: 5.3
Published: 2022-06-07T15:15Z
Repository: https://github.com/discourse/discourse
Language: Ruby
Files changed: app/models/invite_redeemer.rb, spec/models/invite_redeemer_spec.rb
Commit date: 2022-06-03 11:43:52 +0800
Commit message: FIX: Approves user when redeeming an invite for invites only sites (#16984)

When a site has `SiteSetting.invite_only` enabled, we create a
`ReviewableUser`record when activating a user if the user is not
approved. Therefore, we need to approve the user when redeeming an
invite.

There are some uncertainties surrounding why a `ReviewableRecord` is
created for a user in an invites only site but this commit does not seek
to address that.

Follow-up to 7c4e2d33fa4b922354c177ffc880a2f2701a91f9
Diff: diff --git a/app/models/invite_redeemer.rb b/app/models/invite_redeemer.rb
index bd0fd609ca69..e046dd032cfa 100644
--- a/app/models/invite_redeemer.rb
+++ b/app/models/invite_redeemer.rb
@@ -38,11 +38,13 @@ InviteRedeemer = Struct.new(:invite, :email, :username, :name, :password, :user_
       trust_level: SiteSetting.default_invitee_trust_level,
       ip_address: ip_address,
       registration_ip_address: ip_address
     }
 
-    if SiteSetting.must_approve_users? && EmailValidator.can_auto_approve_user?(user.email)
+    if (!SiteSetting.must_approve_users && SiteSetting.invite_only) ||
+       (SiteSetting.must_approve_users? && EmailValidator.can_auto_approve_user?(user.email))
+
       ReviewableUser.set_approved_fields!(user, Discourse.system_user)
     end
 
     user_fields = UserField.all
     if user_custom_fields.present? && user_fields.present?
@@ -77,11 +79,10 @@ InviteRedeemer = Struct.new(:invite, :email, :username, :name, :password, :user_
 
     user.save!
     authenticator.finish
 
     if invite.emailed_status != Invite.emailed_status_types[:not_required] && email == invite.email && invite.email_token.present? && email_token == invite.email_token
-      user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:signup])
       user.activate
     end
 
     User.find(user.id)
   end
diff --git a/spec/models/invite_redeemer_spec.rb b/spec/models/invite_redeemer_spec.rb
index e782f84bdce0..1cfc43127c7d 100644
--- a/spec/models/invite_redeemer_spec.rb
+++ b/spec/models/invite_redeemer_spec.rb
@@ -1,8 +1,9 @@
 # frozen_string_literal: true
 
 describe InviteRedeemer do
+  fab!(:admin) { Fabricate(:admin) }
 
   describe '.create_user_from_invite' do
     it "should be created correctly" do
       invite = Fabricate(:invite, email: 'walter.white@email.com')
       user = InviteRedeemer.create_user_from_invite(invite: invite, email: invite.email, username: 'walter', name: 'Walter White')
@@ -81,10 +82,36 @@ describe InviteRedeemer do
       expect(user.name).to eq('Walter White')
       expect(user.email).to eq('walter.white@email.com')
       expect(user.approved).to eq(false)
       expect(user.active).to eq(false)
     end
+
+    it "approves and actives user when redeeming an invite with email token and SiteSetting.invite_only is enabled" do
+      SiteSetting.invite_only = true
+      Jobs.run_immediately!
+
+      invite = Fabricate(:invite,
+        invited_by: admin,
+        email: 'walter.white@email.com',
+        emailed_status: Invite.emailed_status_types[:sent],
+      )
+
+      user = InviteRedeemer.create_user_from_invite(
+        invite: invite,
+        email: invite.email,
+        email_token: invite.email_token,
+        username: 'walter',
+        name: 'Walter White'
+      )
+
+      expect(user.name).to eq("Walter White")
+      expect(user.username).to eq("walter")
+      expect(user.email).to eq("walter.white@email.com")
+      expect(user.approved).to eq(true)
+      expect(user.active).to eq(true)
+      expect(ReviewableUser.count).to eq(0)
+    end
   end
 
   describe "#redeem" do
     fab!(:invite) { Fabricate(:invite, email: "foobar@example.com") }
     let(:name) { 'john snow' }

Vulnerable code:     if SiteSetting.must_approve_users? && EmailValidator.can_auto_approve_user?(user.email)
      user.email_tokens.create!(email: user.email, scope: EmailToken.scopes[:signup])
Fixed code:     if (!SiteSetting.must_approve_users && SiteSetting.invite_only) ||
       (SiteSetting.must_approve_users? && EmailValidator.can_auto_approve_user?(user.email))

  fab!(:admin) { Fabricate(:admin) }

    it "approves and actives user when redeeming an invite with email token and SiteSetting.invite_only is enabled" do
      SiteSetting.invite_only = true
      Jobs.run_immediately!

      invite = Fabricate(:invite,
        invited_by: admin,
        email: 'walter.white@email.com',
        emailed_status: Invite.emailed_status_types[:sent],
      )

      user = InviteRedeemer.create_user_from_invite(
        invite: invite,
        email: invite.email,
        email_token: invite.email_token,
        username: 'walter',
        name: 'Walter White'
      )

      expect(user.name).to eq("Walter White")
      expect(user.username).to eq("walter")
      expect(user.email).to eq("walter.white@email.com")
      expect(user.approved).to eq(true)
      expect(user.active).to eq(true)
      expect(ReviewableUser.count).to eq(0)
    end