CVE-2024-24827: Insufficient Information (NVD-CWE-noinfo)
value: Discourse is an open source platform for community discussion. Without a rate limit on the POST /uploads endpoint, it makes it easier for an attacker to carry out a DoS attack on the server since creating an upload can be a resource intensive process. Do note that the impact varies from site to site as various site settings like `max_image_size_kb`, `max_attachment_size_kb` and `max_image_megapixels` will determine the amount of resources used when creating an upload. The issue is patched in the latest stable, beta and tests-passed version of Discourse. Users are advised to upgrade. Users unable to upgrade should reduce `max_image_size_kb`, `max_attachment_size_kb` and `max_image_megapixels` as smaller uploads require less resources to process. Alternatively, `client_max_body_size` can be reduced in Nginx to prevent large uploads from reaching the server.
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/controllers/uploads_controller.rb, config/site_settings.yml, spec/requests/uploads_controller_spec.rb
Commit date: 2024-03-15 14:24:00 +0800
Commit message: SECURITY: Add rate limits for uploads

Diff: diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index c4c651e94294c..9e9a2c3a34098 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -23,10 +23,17 @@ class UploadsController < ApplicationController
 
   def create
     # capture current user for block later on
     me = current_user
 
+    RateLimiter.new(
+      current_user,
+      "uploads-per-minute",
+      SiteSetting.max_uploads_per_minute,
+      1.minute.to_i,
+    ).performed!
+
     params.permit(:type, :upload_type)
     raise Discourse::InvalidParameters if params[:type].blank? && params[:upload_type].blank?
     # 50 characters ought to be enough for the upload type
     type =
       (params[:upload_type].presence || params[:type].presence).parameterize(separator: "_")[0..50]
diff --git a/config/site_settings.yml b/config/site_settings.yml
index 0b91bd188c4e9..5555c155a8e55 100644
--- a/config/site_settings.yml
+++ b/config/site_settings.yml
@@ -2226,10 +2226,13 @@ rate_limits:
     default: 10
     hidden: true
   max_complete_multipart_per_minute:
     default: 10
     hidden: true
+  max_uploads_per_minute:
+    default: 10
+    hidden: true
 
 developer:
   force_hostname:
     hidden: true
     default: ""
diff --git a/spec/requests/uploads_controller_spec.rb b/spec/requests/uploads_controller_spec.rb
index e0acb6fde097a..1bca77e3db7ce 100644
--- a/spec/requests/uploads_controller_spec.rb
+++ b/spec/requests/uploads_controller_spec.rb
@@ -17,10 +17,36 @@ RSpec.describe UploadsController do
 
       let(:logo) { Rack::Test::UploadedFile.new(logo_file) }
       let(:fake_jpg) { Rack::Test::UploadedFile.new(file_from_fixtures("fake.jpg")) }
       let(:text_file) { Rack::Test::UploadedFile.new(File.new("#{Rails.root}/LICENSE.txt")) }
 
+      context "when rate limited" do
+        before { RateLimiter.enable }
+
+        use_redis_snapshotting
+
+        it "should return 429 response code when maximum number of uploads per minute has been exceeded for a user" do
+          SiteSetting.max_uploads_per_minute = 1
+
+          post "/uploads.json",
+               params: {
+                 file: Rack::Test::UploadedFile.new(logo_file),
+                 type: "avatar",
+               }
+
+          expect(response.status).to eq(200)
+
+          post "/uploads.json",
+               params: {
+                 file: Rack::Test::UploadedFile.new(logo_file),
+                 type: "avatar",
+               }
+
+          expect(response.status).to eq(429)
+        end
+      end
+
       it "expects a type or upload_type" do
         post "/uploads.json", params: { file: logo }
         expect(response.status).to eq(400)
         post "/uploads.json",
              params: {

Fixed code:     RateLimiter.new(
      current_user,
      "uploads-per-minute",
      SiteSetting.max_uploads_per_minute,
      1.minute.to_i,
    ).performed!

  max_uploads_per_minute:
    default: 10
    hidden: true
      context "when rate limited" do
        before { RateLimiter.enable }

        use_redis_snapshotting

        it "should return 429 response code when maximum number of uploads per minute has been exceeded for a user" do
          SiteSetting.max_uploads_per_minute = 1

          post "/uploads.json",
               params: {
                 file: Rack::Test::UploadedFile.new(logo_file),
                 type: "avatar",
               }

          expect(response.status).to eq(200)

          post "/uploads.json",
               params: {
                 file: Rack::Test::UploadedFile.new(logo_file),
                 type: "avatar",
               }

          expect(response.status).to eq(429)
        end
      end