CVE-2024-27085: Insufficient Information (NVD-CWE-noinfo)
value: Discourse is an open source platform for community discussion. In affected versions users that are allowed to invite others can inject arbitrarily large data in parameters used in the invite route. The problem has been patched in the latest version of Discourse. Users are advised to upgrade. Users unable to upgrade should disable invites or restrict access to them using the `invite allowed groups` site setting. 
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-03-15T20:15Z
Repository: https://github.com/discourse/discourse
Language: Ruby
Files changed: app/models/invite.rb, config/locales/server.en.yml, spec/requests/invites_controller_spec.rb
Commit date: 2024-03-15 14:24:07 +0800
Commit message: SECURITY: Limit invites params length

Diff: diff --git a/app/models/invite.rb b/app/models/invite.rb
index e0bd78ad6ff3f..2aa7c91f6fdaa 100644
--- a/app/models/invite.rb
+++ b/app/models/invite.rb
@@ -30,12 +30,13 @@ class Invite < ActiveRecord::Base
   has_many :groups, through: :invited_groups
   has_many :topic_invites
   has_many :topics, through: :topic_invites, source: :topic
 
   validates_presence_of :invited_by_id
-  validates :email, email: true, allow_blank: true
+  validates :email, email: true, allow_blank: true, length: { maximum: 500 }
   validates :custom_message, length: { maximum: 1000 }
+  validates :domain, length: { maximum: 500 }
   validate :ensure_max_redemptions_allowed
   validate :valid_redemption_count
   validate :valid_domain, if: :will_save_change_to_domain?
   validate :user_doesnt_already_exist, if: :will_save_change_to_email?
   validate :email_xor_domain
@@ -348,11 +349,11 @@ class Invite < ActiveRecord::Base
     return if self.domain.blank?
 
     self.domain.downcase!
 
     if self.domain !~ Invite::DOMAIN_REGEX
-      self.errors.add(:base, I18n.t("invite.domain_not_allowed"))
+      self.errors.add(:base, I18n.t("invite.domain_not_allowed_admin"))
     end
   end
 
   def user_exists_error_msg(email)
     error_key = SiteSetting.hide_email_address_taken? ? "generic_error_response" : "user_exists"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 3a815f4d86d1f..3cd66cc11dedc 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -340,10 +340,11 @@ en:
       discourse_connect_enabled: "Invites are disabled because DiscourseConnect is enabled."
       invalid_access: "You are not permitted to view the requested resource."
     requires_groups: "Invite was not saved because the specified topic is inaccessible. Add one of the following groups: %{groups}."
     max_invite_emails_limit_exceeded: "Request failed because number of emails exceeded the maximum (%{max})."
     domain_not_allowed: "Your email cannot be used to redeem this invite."
+    domain_not_allowed_admin: "This domain cannot be used for invites."
     max_redemptions_allowed_one: "for email invites should be 1."
     redemption_count_less_than_max: "should be less than %{max_redemptions_allowed}."
     email_xor_domain: "Email and domain fields are not allowed at the same time"
     existing_user_success: "Invite redeemed successfully"
 
diff --git a/spec/requests/invites_controller_spec.rb b/spec/requests/invites_controller_spec.rb
index 534d11354d178..2fd40b192dee6 100644
--- a/spec/requests/invites_controller_spec.rb
+++ b/spec/requests/invites_controller_spec.rb
@@ -431,10 +431,22 @@ RSpec.describe InvitesController do
           expect(response).to have_http_status :unprocessable_entity
           expect(response.parsed_body["errors"]).to be_present
         end
       end
 
+      context "when email address is too long" do
+        let(:email) { "a" * 495 + "@example.com" }
+
+        it "fails" do
+          create_invite
+          expect(response).to have_http_status :unprocessable_entity
+          expect(response.parsed_body["errors"]).to be_present
+          error_message = response.parsed_body["errors"].first
+          expect(error_message).to eq("Email is too long (maximum is 500 characters)")
+        end
+      end
+
       context "when providing an email belonging to an existing user" do
         let(:email) { user.email }
 
         before { SiteSetting.hide_email_address_taken = hide_email_address_taken }
 
@@ -458,10 +470,50 @@ RSpec.describe InvitesController do
           end
         end
       end
     end
 
+    context "with domain invite" do
+      it "works" do
+        sign_in(admin)
+
+        post "/invites.json", params: { domain: "example.com" }
+        expect(response).to have_http_status :ok
+      end
+
+      it "fails when domain is invalid" do
+        sign_in(admin)
+
+        post "/invites.json", params: { domain: "example" }
+
+        expect(response).to have_http_status :unprocessable_entity
+
+        error_message = response.parsed_body["errors"].first
+        expect(error_message).to eq(I18n.t("invite.domain_not_allowed_admin"))
+      end
+
+      it "fails when domain is too long" do
+        sign_in(admin)
+
+        post "/invites.json", params: { domain: "a" * 500 + ".ca" }
+        expect(response).to have_http_status :unprocessable_entity
+
+        error_message = response.parsed_body["errors"].first
+        expect(error_message).to eq("Domain is too long (maximum is 500 characters)")
+      end
+
+      it "fails when custom message is too long" do
+        sign_in(admin)
+
+        post "/invites.json", params: { custom_message: "b" * 1001, domain: "example.com" }
+        expect(response).to have_http_status :unprocessable_entity
+
+        error_message = response.parsed_body["errors"].first
+        expect(error_message).to eq("Custom message is too long (maximum is 1000 characters)")
+      end
+    end
+
     context "with link invite" do
       it "works" do
         sign_in(admin)
 
         post "/invites.json"

Vulnerable code:   validates :email, email: true, allow_blank: true
      self.errors.add(:base, I18n.t("invite.domain_not_allowed"))
Fixed code:   validates :email, email: true, allow_blank: true, length: { maximum: 500 }
  validates :domain, length: { maximum: 500 }
      self.errors.add(:base, I18n.t("invite.domain_not_allowed_admin"))
    domain_not_allowed_admin: "This domain cannot be used for invites."
      context "when email address is too long" do
        let(:email) { "a" * 495 + "@example.com" }

        it "fails" do
          create_invite
          expect(response).to have_http_status :unprocessable_entity
          expect(response.parsed_body["errors"]).to be_present
          error_message = response.parsed_body["errors"].first
          expect(error_message).to eq("Email is too long (maximum is 500 characters)")
        end
      end

    context "with domain invite" do
      it "works" do
        sign_in(admin)

        post "/invites.json", params: { domain: "example.com" }
        expect(response).to have_http_status :ok
      end

      it "fails when domain is invalid" do
        sign_in(admin)

        post "/invites.json", params: { domain: "example" }

        expect(response).to have_http_status :unprocessable_entity

        error_message = response.parsed_body["errors"].first
        expect(error_message).to eq(I18n.t("invite.domain_not_allowed_admin"))
      end

      it "fails when domain is too long" do
        sign_in(admin)

        post "/invites.json", params: { domain: "a" * 500 + ".ca" }
        expect(response).to have_http_status :unprocessable_entity

        error_message = response.parsed_body["errors"].first
        expect(error_message).to eq("Domain is too long (maximum is 500 characters)")
      end

      it "fails when custom message is too long" do
        sign_in(admin)

        post "/invites.json", params: { custom_message: "b" * 1001, domain: "example.com" }
        expect(response).to have_http_status :unprocessable_entity

        error_message = response.parsed_body["errors"].first
        expect(error_message).to eq("Custom message is too long (maximum is 1000 characters)")
      end
    end