CVE-2023-28107 (Ruby, published 2023-03-17T17:15Z)
Description: value: Discourse is an open-source discussion platform. Prior to version 3.0.2 of the `stable` branch and version 3.1.0.beta3 of the `beta` and `tests-passed` branches, a user logged as an administrator can request backups multiple times, which will eat up all the connections to the DB. If this is done on a site using multisite, then it can affect the whole cluster. The vulnerability is patched in version 3.0.2 of the `stable` branch and version 3.1.0.beta3 of the `beta` and `tests-passed` branches. There are no known workarounds.
Severity: nan (CVSS v3 base score: 4.9)
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:
app/controllers/admin/backups_controller.rb
lib/backup_restore/backuper.rb
spec/lib/backup_restore/backuper_spec.rb
spec/requests/admin/backups_controller_spec.rb
Commit message: SECURITY: Rate limit the creation of backups


Vulnerable code:
    attr_reader :success
      @success = false
      publish_completion(@success)
      return unless @store.remote?
      @store.upload_file(@backup_filename, archive_path, content_type)
      @store.delete_old
      return if @success && @user.id == Discourse::SYSTEM_USER_ID
      status = @success ? :backup_succeeded : :backup_failed
      refresh_disk_space
      return unless @store.remote?
      @store.reset_cache
    def publish_completion(success)
  it "returns a non-empty parameterized title when site title contains unicode" do
    SiteSetting.title = "Ɣ"
    backuper = BackupRestore::Backuper.new(Discourse.system_user.id)
    expect(backuper.send(:get_parameterized_title)).to eq("discourse")
  end
  it "returns a valid parameterized site title" do
    SiteSetting.title = "Coding Horror"
    backuper = BackupRestore::Backuper.new(Discourse.system_user.id)
    expect(backuper.send(:get_parameterized_title)).to eq("coding-horror")
      before { sign_in(admin) }

Fixed code:
    RateLimiter.new(
      current_user,
      "max-backups-per-minute",
      1,
      1.minute,
      apply_limit_to_staff: true,
    ).performed!

    attr_reader :success, :store
      publish_completion
      return unless store.remote?
      store.upload_file(@backup_filename, archive_path, content_type)
      store.delete_old
      return if success && @user.id == Discourse::SYSTEM_USER_ID
      status = success ? :backup_succeeded : :backup_failed
      refresh_disk_space if success
      return unless store.remote?
      store.reset_cache
    def publish_completion
  describe "#get_parameterized_title" do
    it "returns a non-empty parameterized title when site title contains unicode" do
      SiteSetting.title = "Ɣ"
      backuper = BackupRestore::Backuper.new(Discourse.system_user.id)
      expect(backuper.send(:get_parameterized_title)).to eq("discourse")
    end
    it "returns a valid parameterized site title" do
      SiteSetting.title = "Coding Horror"
      backuper = BackupRestore::Backuper.new(Discourse.system_user.id)
      expect(backuper.send(:get_parameterized_title)).to eq("coding-horror")
    end

  describe "#run" do
    subject(:run) { backup.run }

    let(:backup) { described_class.new(user.id) }
    let(:user) { Discourse.system_user }
    let(:store) { backup.store }

    before { backup.stubs(:success).returns(success) }

    context "when the result isn't successful" do
      let(:success) { false }

      it "doesn't refresh disk stats" do
        store.expects(:reset_cache).never
        run
      end
    end

    context "when the result is successful" do
      let(:success) { true }

      it "refreshes disk stats" do
        store.expects(:reset_cache)
        run
      end
    end
  end
      before do
        sign_in(admin)
        BackupRestore.stubs(:backup!)
      end

      context "with rate limiting enabled" do
        before do
          RateLimiter.clear_all!
          RateLimiter.enable
        end

        after { RateLimiter.disable }

        it "is rate limited" do
          post "/admin/backups.json", params: { with_uploads: false, client_id: "foo" }
          post "/admin/backups.json", params: { with_uploads: false, client_id: "foo" }

          expect(response).to have_http_status :too_many_requests
        end
      end