CVE-2023-23616: Uncontrolled Resource Consumption (CWE-400)
value: Discourse is an open-source discussion platform. Prior to version 3.0.1 on the `stable` branch and 3.1.0.beta2 on the `beta` and `tests-passed` branches, when submitting a membership request, there is no character limit for the reason provided with the request. This could potentially allow a user to flood the database with a large amount of data. However it is unlikely this could be used as part of a DoS attack, as the paths reading back the reasons are only available to administrators. Starting in version 3.0.1 on the `stable` branch and 3.1.0.beta2 on the `beta` and `tests-passed` branches, a limit of 280 characters has been introduced for membership requests.
CWE description: The product does not properly control the allocation and maintenance of a limited resource, thereby enabling an actor to influence the amount of resources consumed, eventually leading to the exhaustion of available resources.
CVSS v3 base score: 4.3
Published: 2023-01-28T00:15Z
Repository: https://github.com/discourse/discourse
Language: Ruby
Files changed: app/assets/javascripts/discourse/app/templates/modal/request-group-membership-form.hbs, app/models/group_request.rb, spec/models/group_request_spec.rb, spec/requests/groups_controller_spec.rb
Commit date: 2023-01-25 13:50:33 +0200
Commit message: SECURITY: Limit the character count of group membership requests (#19993)

When creating a group membership request, there is no character
limit on the 'reason' field. This can be potentially be used by
an attacker to create enormous amount of data in the database.

Co-authored-by: Ted Johansson <ted@discourse.org>
Diff: diff --git a/app/assets/javascripts/discourse/app/templates/modal/request-group-membership-form.hbs b/app/assets/javascripts/discourse/app/templates/modal/request-group-membership-form.hbs
index dbd26377b8f7..3d171b6f678e 100644
--- a/app/assets/javascripts/discourse/app/templates/modal/request-group-membership-form.hbs
+++ b/app/assets/javascripts/discourse/app/templates/modal/request-group-membership-form.hbs
@@ -3,11 +3,11 @@
     <div class="control-group">
       <label>
         {{i18n "groups.membership_request.reason"}}
       </label>
 
-      <ExpandingTextArea @value={{this.reason}} />
+      <ExpandingTextArea @value={{this.reason}} @maxlength="280" />
     </div>
   </DModalBody>
 
   <div class="modal-footer">
     <DButton
diff --git a/app/models/group_request.rb b/app/models/group_request.rb
index 706fcceab976..c923622aac37 100644
--- a/app/models/group_request.rb
+++ b/app/models/group_request.rb
@@ -1,10 +1,14 @@
 # frozen_string_literal: true
 
 class GroupRequest < ActiveRecord::Base
+  REASON_CHARACTER_LIMIT = 280
+
   belongs_to :group
   belongs_to :user
+
+  validates :reason, length: { maximum: REASON_CHARACTER_LIMIT }
 end
 
 # == Schema Information
 #
 # Table name: group_requests
diff --git a/spec/models/group_request_spec.rb b/spec/models/group_request_spec.rb
new file mode 100644
index 000000000000..3dd9a660a311
--- /dev/null
+++ b/spec/models/group_request_spec.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+RSpec.describe GroupRequest do
+  it { is_expected.to belong_to :user }
+  it { is_expected.to belong_to :group }
+
+  it do
+    is_expected.to validate_length_of(:reason).is_at_most(described_class::REASON_CHARACTER_LIMIT)
+  end
+end
diff --git a/spec/requests/groups_controller_spec.rb b/spec/requests/groups_controller_spec.rb
index 6b432c1b2a7d..87c7ad0628ea 100644
--- a/spec/requests/groups_controller_spec.rb
+++ b/spec/requests/groups_controller_spec.rb
@@ -2196,10 +2196,24 @@ RSpec.describe GroupsController do
       post "/groups/#{group.name}/request_membership.json", params: { reason: "Please add me in" }
 
       expect(response.status).to eq(409)
     end
 
+    it "limits the character count of the reason" do
+      sign_in(user)
+
+      post "/groups/#{group.name}/request_membership.json",
+           params: {
+             reason: "x" * (GroupRequest::REASON_CHARACTER_LIMIT + 1),
+           }
+
+      expect(response.status).to eq(422)
+      expect(response.parsed_body["errors"]).to contain_exactly(
+        "Reason is too long (maximum is 280 characters)",
+      )
+    end
+
     it "should create the right PM" do
       owner1 = Fabricate(:user, last_seen_at: Time.zone.now)
       owner2 = Fabricate(:user, last_seen_at: Time.zone.now - 1.day)
       [owner1, owner2].each { |owner| group.add_owner(owner) }
 

Vulnerable code:       <ExpandingTextArea @value={{this.reason}} />
Fixed code:       <ExpandingTextArea @value={{this.reason}} @maxlength="280" />
  REASON_CHARACTER_LIMIT = 280


  validates :reason, length: { maximum: REASON_CHARACTER_LIMIT }
# frozen_string_literal: true

RSpec.describe GroupRequest do
  it { is_expected.to belong_to :user }
  it { is_expected.to belong_to :group }

  it do
    is_expected.to validate_length_of(:reason).is_at_most(described_class::REASON_CHARACTER_LIMIT)
  end
end
    it "limits the character count of the reason" do
      sign_in(user)

      post "/groups/#{group.name}/request_membership.json",
           params: {
             reason: "x" * (GroupRequest::REASON_CHARACTER_LIMIT + 1),
           }

      expect(response.status).to eq(422)
      expect(response.parsed_body["errors"]).to contain_exactly(
        "Reason is too long (maximum is 280 characters)",
      )
    end