CVE-2022-41913 (Ruby, published 2022-11-14T21:15Z)
Description: value: Discourse-calendar is a plugin for the Discourse messaging platform which adds the ability to create a dynamic calendar in the first post of a topic. Members of private groups or public groups with private members can be listed by users, who can create and edit post events. This vulnerability only affects sites which have discourse post events enabled. This issue has been patched in commit `ca5ae3e7e` which will be included in future releases. Users unable to upgrade should disable the `discourse_post_event_enabled` setting to fully mitigate the issue. Also, it's possible to prevent regular users from using this vulnerability by removing all groups from the `discourse_post_event_allowed_on_groups` but note that moderators will still be able to use it.
Severity: nan (CVSS v3 base score: 5.4)
Weakness: NVD-CWE-noinfo \u2014 Insufficient Information
There is insufficient information about the issue to classify it; details are unkown or unspecified.
Repository: https://github.com/discourse/discourse-calendar
Files changed:
config/locales/server.en.yml
jobs/regular/discourse_post_event/bulk_invite.rb
lib/discourse_post_event/event_validator.rb
spec/integration/post_spec.rb
spec/requests/events_controller_spec.rb
Commit message: SECURITY: Do not expose private group members (#345)



Vulnerable code:
    let(:user) { Fabricate(:user, admin: true) }
    let(:topic) { Fabricate(:topic, user: user) }
    let(:post1) { Fabricate(:post, user: user, topic: topic) }
    let(:invitee1) { Fabricate(:user) }
    let(:invitee2) { Fabricate(:user) }


Fixed code:
          invalid_allowed_groups: "Invalid allowed groups."
          acting_user_not_allowed_to_invite_these_groups: "Current user is not allowed to invite these groups."
      invitees = filter_out_unavailable_groups(invitees)


    def invitee_groups(invitees)
      Group.where(name: invitees.map { |i| i[:identifier] })
    end

    def filter_out_unavailable_groups(invitees)
      groups = invitee_groups(invitees)
      invitees.filter do |i|
        group = groups.find { |g| g.name === i[:identifier] }

        !group || (@guardian.can_see_group?(group) && @guardian.can_see_group_members?(group))
      end
    end
      return false unless can_invite_groups?(extracted_event)


    private

    def can_invite_groups?(event)
      guardian = Guardian.new(@post.acting_user)
      return true unless event[:"allowed-groups"]

      event[:"allowed-groups"].split(',').each do |group_name|
        group = Group.find_by(name: group_name)

        if !group || !guardian.can_see_group?(group)
          @post.errors.add(:base, I18n.t("discourse_post_event.errors.models.event.invalid_allowed_groups"))
          return false
        end

        if !guardian.can_see_group_members?(group)
          @post.errors.add(:base, I18n.t("discourse_post_event.errors.models.event.acting_user_not_allowed_to_invite_these_groups"))
          return false
        end
      end

      true
    end
            Fabricate(:group, name: "euro")
            Fabricate(:group, name: "america")


    it "rejects private groups in allowedGroups" do
      moderator = Fabricate(:user, moderator: true)
      private_group = Fabricate(
        :group,
        visibility_level: Group.visibility_levels[:owners])

      expect {
        create_post_with_event(moderator, "allowedGroups='#{private_group.name}'")
      }.to raise_error(ActiveRecord::RecordNotSaved)
    end

    it "rejects non-existent groups in allowedGroups" do
      moderator = Fabricate(:user, moderator: true)

      expect {
        create_post_with_event(moderator, "allowedGroups='non-existent_group_name'")
      }.to raise_error(ActiveRecord::RecordNotSaved)
    end

    it "rejects public groups with private members in allowedGroups" do
      moderator = Fabricate(:user, moderator: true)
      public_group_with_private_members = Fabricate(
        :group,
        visibility_level: Group.visibility_levels[:public],
        members_visibility_level: Group.visibility_levels[:owners])

      expect {
        create_post_with_event(moderator, "allowedGroups='#{public_group_with_private_members.name}'")
      }.to raise_error(ActiveRecord::RecordNotSaved)
    end
      let(:user) { Fabricate(:user, admin: true) }
      let(:topic) { Fabricate(:topic, user: user) }
      let(:post1) { Fabricate(:post, user: user, topic: topic) }
      let(:invitee1) { Fabricate(:user) }
      let(:invitee2) { Fabricate(:user) }


    context 'with a private event' do
      let(:moderator) { Fabricate(:user, moderator: true) }
      let(:topic) { Fabricate(:topic, user: moderator) }
      let(:first_post) { Fabricate(:post, user: moderator, topic: topic) }
      let(:private_event) { Fabricate(:event, post: first_post, status: Event.statuses[:private]) }

      before do
        sign_in(moderator)
      end

      context 'when bulk inviting via CSV file' do
        def csv_file(content)
          file = Tempfile.new("invites.csv")
          file.write(content)
          file.rewind
          file
        end

        it "doesn't invite a private group" do
          private_group = Fabricate(:group, visibility_level: Group.visibility_levels[:owners])

          file = csv_file("#{private_group.name},going\n")
          params = { file: fixture_file_upload(file) }
          post "/discourse-post-event/events/#{private_event.id}/csv-bulk-invite.json", { params: params }

          expect(response.status).to eq(200)
          private_event.reload
          expect(private_event.raw_invitees).to be_nil
        end

        it "returns 200 when inviting a non-existent group" do
          file = csv_file("non-existent group name,going\n")
          params = { file: fixture_file_upload(file) }
          post "/discourse-post-event/events/#{private_event.id}/csv-bulk-invite.json", { params: params }

          expect(response.status).to eq(200)
        end

        it "doesn't invite a public group with private members" do
          public_group_with_private_members = Fabricate(
            :group,
            visibility_level: Group.visibility_levels[:public],
            members_visibility_level: Group.visibility_levels[:owners])

          file = csv_file("#{public_group_with_private_members.name},going\n")
          params = { file: fixture_file_upload(file) }
          post "/discourse-post-event/events/#{private_event.id}/csv-bulk-invite.json", { params: params }

          expect(response.status).to eq(200)
          private_event.reload
          expect(private_event.raw_invitees).to be_nil
        end
      end

      context 'when doing bulk inviting via UI' do
        it "doesn't invite a private group" do
          private_group = Fabricate(:group, visibility_level: Group.visibility_levels[:owners])

          params = { invitees: [
            { 'identifier' => private_group.name, 'attendance' => 'going' }
          ] }
          post "/discourse-post-event/events/#{private_event.id}/bulk-invite.json", { params: params }

          expect(response.status).to eq(200)
          private_event.reload
          expect(private_event.raw_invitees).to be_nil
        end

        it "returns 200 when inviting a non-existent group" do
          params = { invitees: [
            { 'identifier' => 'non-existent group name', 'attendance' => 'going' }
          ] }
          post "/discourse-post-event/events/#{private_event.id}/bulk-invite.json", { params: params }

          expect(response.status).to eq(200)
        end

        it "doesn't invite a public group with private members" do
          public_group_with_private_members = Fabricate(
            :group,
            visibility_level: Group.visibility_levels[:public],
            members_visibility_level: Group.visibility_levels[:owners])

          params = { invitees: [
            { 'identifier' => public_group_with_private_members.name, 'attendance' => 'going' }
          ] }
          post "/discourse-post-event/events/#{private_event.id}/bulk-invite.json", { params: params }

          expect(response.status).to eq(200)
          private_event.reload
          expect(private_event.raw_invitees).to be_nil
        end
      end
    end