CVE-2022-39226: Allocation of Resources Without Limits or Throttling (CWE-770)
value: Discourse is an open source discussion platform. In versions prior to 2.8.9 on the `stable` branch and prior to 2.9.0.beta10 on the `beta` and `tests-passed` branches, a malicious actor can add large payloads of text into the Location and Website fields of a user profile, which causes issues for other users when loading that profile. A fix to limit the length of user input for these fields is included in version 2.8.9 on the `stable` branch and version 2.9.0.beta10 on the `beta` and `tests-passed` branches. There are no known workarounds.
CWE description: 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.
CVSS v3 base score: 4.3
Published: 2022-09-29T20:15Z
Repository: https://github.com/discourse/discourse
Language: Ruby
Files changed: app/models/user_profile.rb, db/migrate/20220920044310_enforce_user_profile_max_limits.rb, spec/models/user_profile_spec.rb
Commit date: 2022-09-21 12:07:06 +1000
Commit message: SECURITY: Limit user profile field length (#18302)

Adds limits to location and website fields at model and DB level
to match the bio_raw field limits. A limit cannot be added at the
DB level for bio_raw because it is a postgres text field.

Co-authored-by: Alan Guo Xiang Tan <gxtan1990@gmail.com>
Diff: diff --git a/app/models/user_profile.rb b/app/models/user_profile.rb
index f6b574d1c3b9..e3136c65261b 100644
--- a/app/models/user_profile.rb
+++ b/app/models/user_profile.rb
@@ -11,13 +11,13 @@ class UserProfile < ActiveRecord::Base
 
   has_many :upload_references, as: :target, dependent: :destroy
   has_many :user_profile_views, dependent: :destroy
 
   validates :bio_raw, length: { maximum: 3000 }, watched_words: true
-  validates :website, url: true, allow_blank: true, if: :validate_website?
+  validates :website, url: true, length: { maximum: 3000 }, allow_blank: true, if: :validate_website?
+  validates :location, length: { maximum: 3000 }, watched_words: true
   validates :user, presence: true
-  validates :location, watched_words: true
 
   validate :website_domain_validator, if: :validate_website?
 
   before_save :cook
   before_save :apply_watched_words, if: :location?
@@ -186,12 +186,12 @@ end
 # == Schema Information
 #
 # Table name: user_profiles
 #
 #  user_id                      :integer          not null, primary key
-#  location                     :string
-#  website                      :string
+#  location                     :string(3000)
+#  website                      :string(3000)
 #  bio_raw                      :text
 #  bio_cooked                   :text
 #  dismissed_banner_key         :integer
 #  bio_cooked_version           :integer
 #  badge_granted_title          :boolean          default(FALSE)
diff --git a/db/migrate/20220920044310_enforce_user_profile_max_limits.rb b/db/migrate/20220920044310_enforce_user_profile_max_limits.rb
new file mode 100644
index 000000000000..14d5db8ade8a
--- /dev/null
+++ b/db/migrate/20220920044310_enforce_user_profile_max_limits.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+class EnforceUserProfileMaxLimits < ActiveRecord::Migration[7.0]
+  def change
+    execute "UPDATE user_profiles SET location = LEFT(location, 3000) WHERE location IS NOT NULL"
+    execute "UPDATE user_profiles SET website = LEFT(website, 3000) WHERE website IS NOT NULL"
+
+    change_column :user_profiles, :location, :string, limit: 3000
+    change_column :user_profiles, :website, :string, limit: 3000
+  end
+end
diff --git a/spec/models/user_profile_spec.rb b/spec/models/user_profile_spec.rb
index 5213371d7625..11063657002f 100644
--- a/spec/models/user_profile_spec.rb
+++ b/spec/models/user_profile_spec.rb
@@ -40,10 +40,19 @@ RSpec.describe UserProfile do
             expect { profile.save! }.to change { profile.location }.to eq "word replaced"
           end
         end
       end
 
+      context "when it is > 3000 characters" do
+        before { profile.location = "a" * 3500 }
+
+        it "is not valid" do
+          expect(profile.valid?).to eq(false)
+          expect(profile.errors.full_messages).to include(/Location is too long \(maximum is 3000 characters\)/)
+        end
+      end
+
       context "when it does not contain watched words" do
         it { is_expected.to be_valid }
       end
 
       it "is not cooked" do
@@ -61,10 +70,19 @@ RSpec.describe UserProfile do
           expect(profile.errors[:base].size).to eq(1)
           expect(profile.errors.messages[:base]).to include(/you can't post the word/)
         end
       end
 
+      context "when it is > 3000 characters" do
+        before { profile.bio_raw = "a" * 3500 }
+
+        it "is not valid" do
+          expect(profile.valid?).to eq(false)
+          expect(profile.errors.full_messages).to include(/About Me is too long \(maximum is 3000 characters\)/)
+        end
+      end
+
       context "when it does not contain watched words" do
         it { is_expected.to be_valid }
       end
     end
   end
@@ -127,10 +145,15 @@ RSpec.describe UserProfile do
         SiteSetting.allowed_user_website_domains = "discourse.org"
 
         user_profile.website = 'user - https://forum.example.com/user'
         expect { user_profile.save! }.to raise_error(ActiveRecord::RecordInvalid)
       end
+
+      it "does not allow > 3000 characters" do
+        user_profile.website = "a" * 3500
+        expect(user_profile).to_not be_valid
+      end
     end
 
     describe 'after save' do
       fab!(:user) { Fabricate(:user) }
 

Vulnerable code:   validates :website, url: true, allow_blank: true, if: :validate_website?
  validates :location, watched_words: true
#  location                     :string
#  website                      :string
Fixed code:   validates :website, url: true, length: { maximum: 3000 }, allow_blank: true, if: :validate_website?
  validates :location, length: { maximum: 3000 }, watched_words: true
#  location                     :string(3000)
#  website                      :string(3000)
# frozen_string_literal: true

class EnforceUserProfileMaxLimits < ActiveRecord::Migration[7.0]
  def change
    execute "UPDATE user_profiles SET location = LEFT(location, 3000) WHERE location IS NOT NULL"
    execute "UPDATE user_profiles SET website = LEFT(website, 3000) WHERE website IS NOT NULL"

    change_column :user_profiles, :location, :string, limit: 3000
    change_column :user_profiles, :website, :string, limit: 3000
  end
end
      context "when it is > 3000 characters" do
        before { profile.location = "a" * 3500 }

        it "is not valid" do
          expect(profile.valid?).to eq(false)
          expect(profile.errors.full_messages).to include(/Location is too long \(maximum is 3000 characters\)/)
        end
      end

      context "when it is > 3000 characters" do
        before { profile.bio_raw = "a" * 3500 }

        it "is not valid" do
          expect(profile.valid?).to eq(false)
          expect(profile.errors.full_messages).to include(/About Me is too long \(maximum is 3000 characters\)/)
        end
      end


      it "does not allow > 3000 characters" do
        user_profile.website = "a" * 3500
        expect(user_profile).to_not be_valid
      end