CVE-2023-22740 (Ruby, published 2023-01-27T01:15Z)
Description: value: Discourse is an open source platform for community discussion. Versions prior to 3.1.0.beta1 (beta) (tests-passed) are vulnerable to  Allocation of Resources Without Limits. Users can create chat drafts of an unlimited length, which can cause a denial of service by generating an excessive load on the server. Additionally, an unlimited number of drafts were loaded when loading the user. This issue has been patched in version 2.1.0.beta1 (beta) and (tests-passed). Users should upgrade to the latest version where a limit has been introduced. There are no workarounds available.
Severity: nan (CVSS v3 base score: 6.5)
Weakness: CWE-770 \u2014 Allocation of Resources Without Limits or Throttling
The product allocates a reusable resource or group of resources on behalf of an actor without imposing any restrictions on the size or number of resources that can be allocated, in violation of the intended security policy for that actor.
Repository: https://github.com/discourse/discourse
Files changed:
plugins/chat/app/controllers/chat_controller.rb
plugins/chat/app/models/chat_draft.rb
plugins/chat/assets/javascripts/discourse/services/chat.js
plugins/chat/config/locales/server.en.yml
plugins/chat/config/settings.yml
plugins/chat/db/post_migrate/20230116090324_drop_chat_drafts_over_max_length.rb
plugins/chat/plugin.rb
plugins/chat/spec/fabricators/chat_fabricator.rb
plugins/chat/spec/models/chat_draft_spec.rb
plugins/chat/spec/requests/chat_controller_spec.rb
plugins/chat/spec/serializer/core_ext/current_user_serializer_spec.rb
Commit message: SECURITY: Limit chat drafts length and preloaded count (#19987)

Only allow maximum of `50_000` characters for chat drafts. A hidden `max_chat_draft_length` setting can control this limit. A migration is also provided to delete any abusive draft in the database.

The number of drafts loaded on current user has also been limited and ordered by most recent update.

Note that spec files moved are not directly related to the fix.

Co-authored-by: Joffrey JAFFEUX <j.jaffeux@gmail.com>
Co-authored-by: Régis Hanol <regis@hanol.fr>

Vulnerable code:
      ChatDraft.find_or_initialize_by(user: current_user, chat_channel_id: @chat_channel.id).update(
        data: params[:data],
      )
    ajax("/chat/drafts", { type: "POST", data, ignoreUnsent: false })

Fixed code:
      ChatDraft.find_or_initialize_by(
        user: current_user,
        chat_channel_id: @chat_channel.id,
      ).update!(data: params[:data])

  validate :data_length
  def data_length
    if self.data && self.data.length > SiteSetting.max_chat_draft_length
      self.errors.add(:base, I18n.t("chat.errors.draft_too_long"))
    end
  end
    ajax("/chat/drafts.json", { type: "POST", data, ignoreUnsent: false })
        // we ignore a draft which can't be saved because it's too big
        // and only deal with network error for now
      draft_too_long: "Draft is too long."
  max_chat_draft_length:
    default: 50_000
    hidden: true
# frozen_string_literal: true

class DropChatDraftsOverMaxLength < ActiveRecord::Migration[7.0]
  def up
    if table_exists?(:chat_drafts)
      # Delete abusive drafts
      execute <<~SQL
        DELETE FROM chat_drafts
        WHERE LENGTH(data) > 50000
      SQL
    end
  end

  def down
    raise ActiveRecord::IrreversibleMigration
  end
end
      .order(updated_at: :desc)
      .limit(20)

Fabricator(:chat_draft) do
  user
  chat_channel

  transient :value, "chat draft message"
  transient :uploads, []
  transient :reply_to_msg

  data do |attrs|
    { value: attrs[:value], replyToMsg: attrs[:reply_to_msg], uploads: attrs[:uploads] }.to_json
  end
end
# frozen_string_literal: true

RSpec.describe ChatDraft do
  before { SiteSetting.max_chat_draft_length = 100 }

  it "errors when data.value is greater than `max_chat_draft_length`" do
    draft =
      described_class.create(
        user_id: Fabricate(:user).id,
        chat_channel_id: Fabricate(:chat_channel).id,
        data: { value: "A" * (SiteSetting.max_chat_draft_length + 1) }.to_json,
      )

    expect(draft.errors.full_messages).to eq(
      [I18n.t("chat.errors.draft_too_long", { maximum: SiteSetting.max_chat_draft_length })],
    )
  end
end

    it "cannot create a too long chat draft" do
      SiteSetting.max_chat_draft_length = 100

      post "/chat/drafts.json",
           params: {
             chat_channel_id: chat_channel.id,
             data: { value: "a" * (SiteSetting.max_chat_draft_length + 1) }.to_json,
           }

      expect(response.status).to eq(422)
      expect(response.parsed_body["errors"]).to eq([I18n.t("chat.errors.draft_too_long")])
    end
# frozen_string_literal: true

RSpec.describe CurrentUserSerializer do
  fab!(:current_user) { Fabricate(:user) }

  let(:serializer) do
    described_class.new(current_user, scope: Guardian.new(current_user), root: false)
  end

  before do
    SiteSetting.chat_enabled = true
    SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
    current_user.user_option.update(chat_enabled: true)
  end

  describe "#chat_drafts" do
    context "when user can't chat" do
      before { SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:staff] }

      it "is not present" do
        expect(serializer.as_json[:chat_drafts]).to be_blank
      end
    end

    it "is ordered by most recent drafts" do
      Fabricate(:chat_draft, user: current_user, value: "second draft")
      Fabricate(:chat_draft, user: current_user, value: "first draft")

      values =
        serializer.as_json[:chat_drafts].map { |draft| MultiJson.load(draft[:data])["value"] }
      expect(values).to eq(["first draft", "second draft"])
    end

    it "limits the numbers of drafts" do
      21.times { Fabricate(:chat_draft, user: current_user) }

      expect(serializer.as_json[:chat_drafts].length).to eq(20)
    end
  end
end