CVE-2023-38684 (published 2023-07-28T16:15Z)

Discourse is an open source discussion platform. Prior to version 3.0.6 of the `stable` branch and version 3.1.0.beta7 of the `beta` and `tests-passed` branches, in multiple controller actions, Discourse accepts limit params but does not impose any upper bound on the values being accepted. Without an upper bound, the software may allow arbitrary users to generate DB queries which may end up exhausting the resources on the server. The issue is patched in version 3.0.6 of the `stable` branch and version 3.1.0.beta7 of the `beta` and `tests-passed` branches. There are no known workarounds for this vulnerability.

CVSS v3 base score: 7.5, HIGH (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H)

Weakness information:
cwe_id: CWE-770
cwe_name: Allocation of Resources Without Limits or Throttling
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.
extended_description: Code frequently has to work with limited resources, so programmers must be careful to ensure that resources are not consumed too quickly, or too easily.  Without use of quotas, resource limits, or other protection mechanisms, it can be easy for an attacker to consume many resources by rapidly making many requests, or causing larger resources to be used than is needed. When too many resources are allocated, or if a single resource is too large, then it can prevent the code from working correctly, possibly leading to a denial of service.
         

Fix information:
repo_url: https://github.com/discourse/discourse
commit_details: repo_url: https://github.com/discourse/discourse
author: Alan Guo Xiang Tan
author_date: 2023-07-28 12:53:46+01:00
msg: SECURITY: Impose a upper bound on limit params in various controllers

What is the problem here?

In multiple controllers, we are accepting a `limit` params but do not
impose any upper bound on the values being accepted. Without an upper
bound, we may be allowing arbituary users from generating DB queries
which may end up exhausing the resources on the server.

What is the fix here?

A new `fetch_limit_from_params` helper method is introduced in
`ApplicationController` that can be used by controller actions to safely
get the limit from the params as a default limit and maximum limit has
to be set. When an invalid limit params is encountered, the server will
respond with the 400 response code.
repository: repo_url: https://github.com/discourse/discourse
repo_name: discourse/discourse
description: A platform for community discussion. Free, open, simple.
repo_language: Ruby
owner: discourse
file_changes: filename: api_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -4,9 +4,11 @@ class Admin::ApiController < Admin::AdminController
   # Note: in the REST API, ApiKeys are referred to simply as "key"
   # If we used "api_key", then our user provider would try to use the value for authentication
 
+  INDEX_LIMIT = 50
+
   def index
     offset = (params[:offset] || 0).to_i
-    limit = (params[:limit] || 50).to_i.clamp(1, 50)
+    limit = fetch_limit_from_params(default: INDEX_LIMIT, max: INDEX_LIMIT)
 
     keys =
       ApiKey

programming_language: Ruby
filename: reports_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -1,6 +1,8 @@
 # frozen_string_literal: true
 
 class Admin::ReportsController < Admin::StaffController
+  REPORTS_LIMIT = 50
+
   def index
     reports_methods =
       ["page_view_total_reqs"] +
@@ -108,10 +110,7 @@ class Admin::ReportsController < Admin::StaffController
     facets = nil
     facets = report_params[:facets].map { |s| s.to_s.to_sym } if Array === report_params[:facets]
 
-    limit = nil
-    if report_params.has_key?(:limit) && report_params[:limit].to_i > 0
-      limit = report_params[:limit].to_i
-    end
+    limit = fetch_limit_from_params(params: report_params, default: nil, max: REPORTS_LIMIT)
 
     filters = nil
     filters = report_params[:filters] if report_params.has_key?(:filters)

programming_language: Ruby
filename: staff_action_logs_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -1,11 +1,13 @@
 # frozen_string_literal: true
 
 class Admin::StaffActionLogsController < Admin::StaffController
+  INDEX_LIMIT = 200
+
   def index
     filters = params.slice(*UserHistory.staff_filters + %i[page limit])
 
     page = (params[:page] || 0).to_i
-    page_size = (params[:limit] || 200).to_i.clamp(1, 200)
+    page_size = fetch_limit_from_params(default: INDEX_LIMIT, max: INDEX_LIMIT)
 
     staff_action_logs = UserHistory.staff_action_records(current_user, filters)
     count = staff_action_logs.count

programming_language: Ruby
filename: application_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -1084,4 +1084,22 @@ class ApplicationController < ActionController::Base
         end
       end
   end
+
+  def fetch_limit_from_params(params: self.params, default:, max:)
+    raise "default limit cannot be greater than max limit" if default.present? && default > max
+
+    if params.has_key?(:limit)
+      limit =
+        begin
+          Integer(params[:limit])
+        rescue ArgumentError
+          raise Discourse::InvalidParameters.new(:limit)
+        end
+
+      raise Discourse::InvalidParameters.new(:limit) if limit < 0 || limit > max
+      limit
+    else
+      default
+    end
+  end
 end

programming_language: Ruby
filename: directory_items_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -81,8 +81,7 @@ class DirectoryItemsController < ApplicationController
       end
     end
 
-    limit = [params[:limit].to_i, PAGE_SIZE].min if params[:limit].to_i > 0
-    limit ||= PAGE_SIZE
+    limit = fetch_limit_from_params(default: PAGE_SIZE, max: PAGE_SIZE)
 
     result_count = result.count
     result = result.limit(limit).offset(limit * page).to_a

programming_language: Ruby
filename: drafts_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -5,11 +5,17 @@ class DraftsController < ApplicationController
 
   skip_before_action :check_xhr, :preload_json
 
+  INDEX_LIMIT = 50
+
   def index
     params.permit(:offset)
-    params.permit(:limit)
 
-    stream = Draft.stream(user: current_user, offset: params[:offset], limit: params[:limit])
+    stream =
+      Draft.stream(
+        user: current_user,
+        offset: params[:offset],
+        limit: fetch_limit_from_params(default: nil, max: INDEX_LIMIT),
+      )
 
     render json: { drafts: stream ? serialize_data(stream, DraftSerializer) : [] }
   end

programming_language: Ruby
filename: groups_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -230,15 +230,16 @@ class GroupsController < ApplicationController
     render "posts/latest", formats: [:rss]
   end
 
+  MEMBERS_LIMIT = 1_000
+
   def members
     group = find_group(:group_id)
 
     guardian.ensure_can_see_group_members!(group)
 
-    limit = (params[:limit] || 50).to_i
+    limit = fetch_limit_from_params(default: 50, max: MEMBERS_LIMIT)
     offset = params[:offset].to_i
 
-    raise Discourse::InvalidParameters.new(:limit) if limit < 0 || limit > 1000
     raise Discourse::InvalidParameters.new(:offset) if offset < 0
 
     dir = (params[:asc] && params[:asc].present?) ? "ASC" : "DESC"

programming_language: Ruby
filename: notifications_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -5,6 +5,8 @@ class NotificationsController < ApplicationController
   before_action :ensure_admin, only: %i[create update destroy]
   before_action :set_notification, only: %i[update destroy]
 
+  INDEX_LIMIT = 50
+
   def index
     user =
       if params[:username] && !params[:recent]
@@ -25,8 +27,7 @@ class NotificationsController < ApplicationController
     end
 
     if params[:recent].present?
-      limit = (params[:limit] || 15).to_i
-      limit = 50 if limit > 50
+      limit = fetch_limit_from_params(default: 15, max: INDEX_LIMIT)
 
       include_reviewables = false
 

programming_language: Ruby
filename: post_action_users_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -1,13 +1,15 @@
 # frozen_string_literal: true
 
 class PostActionUsersController < ApplicationController
+  INDEX_LIMIT = 200
+
   def index
     params.require(:post_action_type_id)
     params.require(:id)
     post_action_type_id = params[:post_action_type_id].to_i
 
     page = params[:page].to_i
-    page_size = (params[:limit] || 200).to_i
+    page_size = fetch_limit_from_params(default: INDEX_LIMIT, max: INDEX_LIMIT)
 
     # Find the post, and then determine if they can see the post (if deleted)
     post = Post.with_deleted.where(id: params[:id].to_i).first

programming_language: Ruby
filename: posts_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -662,13 +662,15 @@ class PostsController < ApplicationController
     render body: nil
   end
 
+  DELETED_POSTS_MAX_LIMIT = 100
+
   def deleted_posts
     params.permit(:offset, :limit)
     guardian.ensure_can_see_deleted_posts!
 
     user = fetch_user_from_params
     offset = [params[:offset].to_i, 0].max
-    limit = [(params[:limit] || 60).to_i, 100].min
+    limit = fetch_limit_from_params(default: 60, max: DELETED_POSTS_MAX_LIMIT)
 
     posts = user_posts(guardian, user.id, offset: offset, limit: limit).where.not(deleted_at: nil)
 

programming_language: Ruby
filename: tag_groups_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -79,7 +79,10 @@ class TagGroupsController < ApplicationController
       matches = matches.where("lower(NAME) in (?)", params[:names].map(&:downcase))
     end
 
-    matches = matches.order("name").limit(params[:limit] || 5)
+    matches =
+      matches.order("name").limit(
+        fetch_limit_from_params(default: 5, max: SiteSetting.max_tag_search_results),
+      )
 
     render json: {
              results:

programming_language: Ruby
filename: tags_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -292,13 +292,8 @@ class TagsController < ::ApplicationController
       exclude_has_synonyms: params[:excludeHasSynonyms],
     }
 
-    if params[:limit]
-      begin
-        filter_params[:limit] = Integer(params[:limit])
-        raise Discourse::InvalidParameters.new(:limit) if !filter_params[:limit].positive?
-      rescue ArgumentError
-        raise Discourse::InvalidParameters.new(:limit)
-      end
+    if limit = fetch_limit_from_params(default: nil, max: SiteSetting.max_tag_search_results)
+      filter_params[:limit] = limit
     end
 
     filter_params[:category] = Category.find_by_id(params[:categoryId]) if params[:categoryId]

programming_language: Ruby
filename: users_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -1182,6 +1182,8 @@ class UsersController < ApplicationController
     end
   end
 
+  SEARCH_USERS_LIMIT = 50
+
   def search_users
     term = params[:term].to_s.strip
 
@@ -1204,10 +1206,11 @@ class UsersController < ApplicationController
       params[:include_staged_users],
     )
     options[:last_seen_users] = !!ActiveModel::Type::Boolean.new.cast(params[:last_seen_users])
-    if params[:limit].present?
-      options[:limit] = params[:limit].to_i
-      raise Discourse::InvalidParameters.new(:limit) if options[:limit] <= 0
+
+    if limit = fetch_limit_from_params(default: nil, max: SEARCH_USERS_LIMIT)
+      options[:limit] = limit
     end
+
     options[:topic_id] = topic_id if topic_id
     options[:category_id] = category_id if category_id
 
@@ -1733,6 +1736,8 @@ class UsersController < ApplicationController
     render json: success_json
   end
 
+  BOOKMARKS_LIMIT = 20
+
   def bookmarks
     user = fetch_user_from_params
     guardian.ensure_can_edit!(user)
@@ -1740,7 +1745,15 @@ class UsersController < ApplicationController
 
     respond_to do |format|
       format.json do
-        bookmark_list = UserBookmarkList.new(user: user, guardian: guardian, params: params)
+        bookmark_list =
+          UserBookmarkList.new(
+            user: user,
+            guardian: guardian,
+            search_term: params[:q],
+            page: params[:page],
+            per_page: fetch_limit_from_params(default: nil, max: BOOKMARKS_LIMIT),
+          )
+
         bookmark_list.load
 
         if bookmark_list.bookmarks.empty?
@@ -1789,10 +1802,9 @@ class UsersController < ApplicationController
         UserBookmarkList.new(
           user: current_user,
           guardian: guardian,
-          params: {
-            per_page: USER_MENU_LIST_LIMIT - reminder_notifications.size,
-          },
+          per_page: USER_MENU_LIST_LIMIT - reminder_notifications.size,
         )
+
       bookmark_list.load do |query|
         if exclude_bookmark_ids.present?
           query.where("bookmarks.id NOT IN (?)", exclude_bookmark_ids)

programming_language: Ruby
filename: user_bookmark_list.rb
change_type: ModificationType.MODIFY
diff: @@ -8,26 +8,32 @@ class UserBookmarkList
   attr_reader :bookmarks, :per_page, :has_more
   attr_accessor :more_bookmarks_url, :bookmark_serializer_opts
 
-  def initialize(user:, guardian:, params:)
+  def initialize(user:, guardian:, search_term: nil, per_page: nil, page: 0)
     @user = user
     @guardian = guardian
-    @params = params
 
-    @params.merge!(per_page: PER_PAGE) if params[:per_page].blank?
-    @params[:per_page] = PER_PAGE if @params[:per_page] > PER_PAGE
+    @per_page = per_page || PER_PAGE
+    @per_page = PER_PAGE if @per_page > PER_PAGE
+
+    @search_term = search_term
+    @page = page.to_i
 
     @bookmarks = []
     @bookmark_serializer_opts = {}
   end
 
   def load(&blk)
-    query = BookmarkQuery.new(user: @user, guardian: @guardian, params: @params)
+    query =
+      BookmarkQuery.new(
+        user: @user,
+        guardian: @guardian,
+        search_term: @search_term,
+        page: @page,
+        per_page: @per_page,
+      )
+
     @bookmarks = query.list_all(&blk)
-    @has_more = (@params[:page].to_i + 1) * @params[:per_page] < query.count
+    @has_more = (@page.to_i + 1) * @per_page < query.count
     @bookmarks
   end
-
-  def per_page
-    @per_page ||= @params[:per_page]
-  end
 end

programming_language: Ruby
filename: bookmark_query.rb
change_type: ModificationType.MODIFY
diff: @@ -26,19 +26,18 @@ class BookmarkQuery
 
   attr_reader :guardian, :count
 
-  def initialize(user:, guardian: nil, params: {})
+  def initialize(user:, guardian: nil, search_term: nil, page: nil, per_page: nil)
     @user = user
-    @params = params
+    @search_term = search_term
     @guardian = guardian || Guardian.new(@user)
-    @page = @params[:page].to_i
-    @limit = @params[:limit].present? ? @params[:limit].to_i : @params[:per_page]
+    @page = page ? page.to_i : 0
+    @per_page = per_page ? per_page.to_i : 20
     @count = 0
   end
 
   def list_all(&blk)
-    search_term = @params[:q]
-    ts_query = search_term.present? ? Search.ts_query(term: search_term) : nil
-    search_term_wildcard = search_term.present? ? "%#{search_term}%" : nil
+    ts_query = @search_term.present? ? Search.ts_query(term: @search_term) : nil
+    search_term_wildcard = @search_term.present? ? "%#{@search_term}%" : nil
 
     queries =
       Bookmark
@@ -51,7 +50,7 @@ class BookmarkQuery
           # on a topic and that topic was moved into a private category
           next if interim_results.blank?
 
-          if search_term.present?
+          if @search_term.present?
             interim_results =
               bookmarkable.perform_search_query(interim_results, search_term_wildcard, ts_query)
           end
@@ -78,13 +77,14 @@ class BookmarkQuery
 
     @count = results.count
 
-    results = results.offset(@page * @params[:per_page]) if @page.positive?
+    results = results.offset(@page * @per_page) if @page.positive?
 
     if updated_results = blk&.call(results)
       results = updated_results
     end
 
-    results = results.limit(@limit).to_a
+    results = results.limit(@per_page).to_a
+
     BookmarkQuery.preload(results, self)
     results
   end

programming_language: Ruby
filename: channels_memberships_controller.rb
change_type: ModificationType.MODIFY
diff: @@ -1,11 +1,13 @@
 # frozen_string_literal: true
 
 class Chat::Api::ChannelsMembershipsController < Chat::Api::ChannelsController
+  INDEX_LIMIT = 50
+
   def index
     params.permit(:username, :offset, :limit)
 
     offset = params[:offset].to_i
-    limit = (params[:limit] || 50).to_i.clamp(1, 50)
+    limit = fetch_limit_from_params(default: INDEX_LIMIT, max: INDEX_LIMIT)
 
     memberships =
       Chat::ChannelMembershipsQuery.call(

programming_language: Ruby
filename: bookmark_query_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -4,10 +4,9 @@ RSpec.describe BookmarkQuery do
   before { SearchIndexer.enable }
 
   fab!(:user) { Fabricate(:user) }
-  let(:params) { {} }
 
-  def bookmark_query(user: nil, params: nil)
-    BookmarkQuery.new(user: user || self.user, params: params || self.params)
+  def bookmark_query(user: nil, search_term: nil, per_page: nil)
+    BookmarkQuery.new(user: user || self.user, search_term:, per_page:)
   end
 
   describe "#list_all" do
@@ -67,7 +66,7 @@ RSpec.describe BookmarkQuery do
       expect(bookmarks.map(&:id)).to eq([])
     end
 
-    context "when q param is provided" do
+    context "when search_term is provided" do
       let!(:post) do
         Fabricate(
           :post,
@@ -89,17 +88,17 @@ RSpec.describe BookmarkQuery do
       end
 
       it "can search by bookmark name" do
-        bookmarks = bookmark_query(params: { q: "check" }).list_all
+        bookmarks = bookmark_query(search_term: "check").list_all
         expect(bookmarks.map(&:id)).to eq([bookmark3.id])
       end
 
       it "can search by post content" do
-        bookmarks = bookmark_query(params: { q: "content" }).list_all
+        bookmarks = bookmark_query(search_term: "content").list_all
         expect(bookmarks.map(&:id)).to eq([bookmark4.id])
       end
 
       it "can search by topic title" do
-        bookmarks = bookmark_query(params: { q: "bugfix" }).list_all
+        bookmarks = bookmark_query(search_term: "bugfix").list_all
         expect(bookmarks.map(&:id)).to eq([bookmark4.id])
       end
 
@@ -111,7 +110,7 @@ RSpec.describe BookmarkQuery do
         end
 
         it "allows searching bookmarkables by fields in other tables" do
-          bookmarks = bookmark_query(params: { q: "bookmarkk" }).list_all
+          bookmarks = bookmark_query(search_term: "bookmarkk").list_all
           expect(bookmarks.map(&:id)).to eq([bookmark5.id])
         end
       end
@@ -205,10 +204,9 @@ RSpec.describe BookmarkQuery do
       end
     end
 
-    context "when the limit param is provided" do
-      let(:params) { { limit: 1 } }
+    context "when the per_page is provided" do
       it "is respected" do
-        expect(bookmark_query.list_all.count).to eq(1)
+        expect(bookmark_query(per_page: 1).list_all.count).to eq(1)
       end
     end
   end

programming_language: Ruby
filename: user_bookmark_list_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -1,9 +1,8 @@
 # frozen_string_literal: true
 
 RSpec.describe UserBookmarkList do
-  let(:params) { {} }
   fab!(:user) { Fabricate(:user) }
-  let(:list) { UserBookmarkList.new(user: user, guardian: Guardian.new(user), params: params) }
+  let(:list) { UserBookmarkList.new(user: user, guardian: Guardian.new(user)) }
 
   before do
     register_test_bookmarkable
@@ -21,9 +20,11 @@ RSpec.describe UserBookmarkList do
 
   it "returns all types of bookmarks" do
     list.load
+
     expect(list.bookmarks.map(&:id)).to match_array(
       [post_bookmark.id, topic_bookmark.id, user_bookmark.id],
     )
+
     expect(list.has_more).to eq(false)
   end
 
@@ -32,13 +33,14 @@ RSpec.describe UserBookmarkList do
   end
 
   context "when the per_page param is too high" do
-    let(:params) { { per_page: 1000 } }
-
     it "does not allow more than X bookmarks to be requested per page" do
       22.times do
         bookmark = Fabricate(:bookmark, user: user, bookmarkable: Fabricate(:post))
         Fabricate(:topic_user, topic: bookmark.bookmarkable.topic, user: user)
       end
+
+      list = UserBookmarkList.new(user: user, guardian: Guardian.new(user), per_page: 1000)
+
       expect(list.load.count).to eq(20)
     end
   end

programming_language: Ruby
filename: rails_helper.rb
change_type: ModificationType.MODIFY
diff: @@ -78,6 +78,7 @@ end
 # Requires supporting ruby files with custom matchers and macros, etc,
 # in spec/support/ and its subdirectories.
 Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
+Dir[Rails.root.join("spec/requests/examples/*.rb")].each { |f| require f }
 
 Dir[Rails.root.join("spec/system/helpers/**/*.rb")].each { |f| require f }
 Dir[Rails.root.join("spec/system/page_objects/**/base.rb")].each { |f| require f }

programming_language: Ruby
filename: api_controller_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -20,6 +20,12 @@ RSpec.describe Admin::ApiController do
         expect(response.parsed_body["keys"].length).to eq(3)
       end
 
+      describe "when limit params is invalid" do
+        include_examples "invalid limit params",
+                         "/admin/api/keys.json",
+                         described_class::INDEX_LIMIT
+      end
+
       it "can paginate results" do
         get "/admin/api/keys.json?offset=0&limit=2"
 

programming_language: Ruby
filename: reports_controller_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -30,6 +30,12 @@ RSpec.describe Admin::ReportsController do
       end
 
       context "with invalid params" do
+        context "when limit param is invalid" do
+          include_examples "invalid limit params",
+                           "/admin/reports/topics.json",
+                           described_class::REPORTS_LIMIT
+        end
+
         context "with nonexistent report" do
           it "returns not found reports" do
             get "/admin/reports/bulk.json",
@@ -154,6 +160,12 @@ RSpec.describe Admin::ReportsController do
             expect(response.parsed_body["report"]["total"]).to eq(1)
           end
         end
+
+        context "when limit param is invalid" do
+          include_examples "invalid limit params",
+                           "/admin/reports/topics.json",
+                           described_class::REPORTS_LIMIT
+        end
       end
 
       describe "when report is scoped to a category" do

programming_language: Ruby
filename: staff_action_logs_controller_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -27,6 +27,12 @@ RSpec.describe Admin::StaffActionLogsController do
           "action_id" => UserHistory.actions[:delete_topic],
         )
       end
+
+      describe "when limit params is invalid" do
+        include_examples "invalid limit params",
+                         "/admin/logs/staff_action_logs.json",
+                         described_class::INDEX_LIMIT
+      end
     end
 
     context "when logged in as an admin" do

programming_language: Ruby
filename: directory_items_controller_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -30,22 +30,7 @@ RSpec.describe DirectoryItemsController do
       expect(json["directory_items"].length).to eq(2)
     end
 
-    it "does not exceed PAGE_SIZE if limit parameter is more than PAGE_SIZE" do
-      large_limit = DirectoryItemsController::PAGE_SIZE + 10
-      get "/directory_items.json", params: { period: "all", limit: large_limit }
-      expect(response.status).to eq(200)
-      json = response.parsed_body
-
-      expect(json["directory_items"].length).to eq(DirectoryItemsController::PAGE_SIZE)
-    end
-
-    it "handles invalid limit parameters gracefully" do
-      get "/directory_items.json", params: { period: "all", limit: "invalid_limit" }
-      expect(response.status).to eq(200)
-      json = response.parsed_body
-
-      expect(json["directory_items"]).not_to be_empty
-    end
+    include_examples "invalid limit params", "/directory_items.json", described_class::PAGE_SIZE
   end
 
   context "with exclude_groups parameter" do

programming_language: Ruby
filename: drafts_controller_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -1,14 +1,22 @@
 # frozen_string_literal: true
 
 RSpec.describe DraftsController do
+  fab!(:user) { Fabricate(:user) }
+
   describe "#index" do
     it "requires you to be logged in" do
       get "/drafts.json"
       expect(response.status).to eq(403)
     end
 
+    describe "when limit params is invalid" do
+      before { sign_in(user) }
+
+      include_examples "invalid limit params", "/drafts.json", described_class::INDEX_LIMIT
+    end
+
     it "returns correct stream length after adding a draft" do
-      user = sign_in(Fabricate(:user))
+      sign_in(user)
       Draft.set(user, "xxx", 0, "{}")
       get "/drafts.json"
       expect(response.status).to eq(200)
@@ -17,7 +25,7 @@ RSpec.describe DraftsController do
     end
 
     it "has empty stream after deleting last draft" do
-      user = sign_in(Fabricate(:user))
+      sign_in(user)
       Draft.set(user, "xxx", 0, "{}")
       Draft.clear(user, "xxx", 0)
       get "/drafts.json"
@@ -46,7 +54,7 @@ RSpec.describe DraftsController do
 
   describe "#show" do
     it "returns a draft if requested" do
-      user = sign_in(Fabricate(:user))
+      sign_in(user)
       Draft.set(user, "hello", 0, "test")
 
       get "/drafts/hello.json"
@@ -62,7 +70,7 @@ RSpec.describe DraftsController do
     end
 
     it "saves a draft" do
-      user = sign_in(Fabricate(:user))
+      sign_in(user)
 
       post "/drafts.json", params: { draft_key: "xyz", data: { my: "data" }.to_json, sequence: 0 }
 
@@ -77,7 +85,7 @@ RSpec.describe DraftsController do
     end
 
     it "checks for an conflict on update" do
-      user = sign_in(Fabricate(:user))
+      sign_in(user)
       post = Fabricate(:post, user: user)
 
       post "/drafts.json",
@@ -103,7 +111,7 @@ RSpec.describe DraftsController do
     end
 
     it "cant trivially resolve conflicts without interaction" do
-      user = sign_in(Fabricate(:user))
+      sign_in(user)
 
       DraftSequence.next!(user, "abc")
 
@@ -120,7 +128,7 @@ RSpec.describe DraftsController do
     end
 
     it "has a clean protocol for ownership handover" do
-      user = sign_in(Fabricate(:user))
+      sign_in(user)
 
       post "/drafts.json",
            params: {
@@ -170,7 +178,7 @@ RSpec.describe DraftsController do
     end
 
     it "raises an error for out-of-sequence draft setting" do
-      user = sign_in(Fabricate(:user))
+      sign_in(user)
       seq = DraftSequence.next!(user, "abc")
       Draft.set(user, "abc", seq, { b: "test" }.to_json)
 
@@ -230,7 +238,7 @@ RSpec.describe DraftsController do
 
   describe "#destroy" do
     it "destroys drafts when required" do
-      user = sign_in(Fabricate(:user))
+      sign_in(user)
       Draft.set(user, "xxx", 0, "hi")
       delete "/drafts/xxx.json", params: { sequence: 0 }
 

programming_language: Ruby
filename: invalid_limit_params.rb
change_type: ModificationType.ADD
diff: @@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+RSpec.shared_examples "invalid limit params" do |endpoint, max_limit, extra_params|
+  let(:params) { extra_params&.dig(:params) || {} }
+
+  it "returns 400 response code when limit params is negative" do
+    get endpoint, params: { limit: -1 }.merge(params)
+
+    expect(response.status).to eq(400)
+  end
+
+  it "returns 400 response code when limit params is suspicious" do
+    get endpoint, params: { limit: "1; DROP TABLE users" }.merge(params)
+
+    expect(response.status).to eq(400)
+  end
+
+  it "returns 400 response code when limit params exceeds the max limit" do
+    get endpoint, params: { limit: max_limit + 1 }.merge(params)
+
+    expect(response.status).to eq(400)
+  end
+end

programming_language: Ruby
filename: notifications_controller_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -87,6 +87,15 @@ RSpec.describe NotificationsController do
           Discourse.clear_redis_readonly!
         end
 
+        describe "when limit params is invalid" do
+          include_examples "invalid limit params",
+                           "/notifications.json",
+                           described_class::INDEX_LIMIT,
+                           params: {
+                             recent: true,
+                           }
+        end
+
         it "get notifications with all filters" do
           notification = Fabricate(:notification, user: user)
           notification2 = Fabricate(:notification, user: user)

programming_language: Ruby
filename: post_action_users_controller_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -4,6 +4,14 @@ RSpec.describe PostActionUsersController do
   fab!(:user) { Fabricate(:user) }
   let(:post) { Fabricate(:post, user: sign_in(user)) }
 
+  describe "index" do
+    describe "when limit params is invalid" do
+      include_examples "invalid limit params",
+                       "/post_action_users.json",
+                       described_class::INDEX_LIMIT
+    end
+  end
+
   context "with render" do
     it "always allows you to see your own actions" do
       notify_mod = PostActionType.types[:notify_moderators]

programming_language: Ruby
filename: posts_controller_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -2254,6 +2254,14 @@ RSpec.describe PostsController do
         expect(response).to be_forbidden
       end
 
+      describe "when limit params is invalid" do
+        before { sign_in(moderator) }
+
+        include_examples "invalid limit params",
+                         "/posts/system/deleted.json",
+                         described_class::DELETED_POSTS_MAX_LIMIT
+      end
+
       it "can see the deleted posts when authorized" do
         sign_in(moderator)
         get "/posts/system/deleted.json"

programming_language: Ruby
filename: tag_groups_controller_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -48,6 +48,12 @@ RSpec.describe TagGroupsController do
     let(:full) { TagGroupPermission.permission_types[:full] }
     let(:readonly) { TagGroupPermission.permission_types[:readonly] }
 
+    describe "when limit params is invalid" do
+      include_examples "invalid limit params",
+                       "/tag_groups/filter/search.json",
+                       SiteSetting.max_tag_search_results
+    end
+
     context "for anons" do
       it "returns the tag group with the associated tag names" do
         tag_group = tag_group_with_permission(everyone, readonly)

programming_language: Ruby
filename: tags_controller_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -1103,22 +1103,10 @@ RSpec.describe TagsController do
         )
       end
 
-      it "returns error 400 for negative limit" do
-        get "/tags/filter/search.json", params: { q: "", limit: -1 }
-
-        expect(response.status).to eq(400)
-        expect(response.parsed_body["errors"].first).to eq(
-          I18n.t("invalid_params", message: "limit"),
-        )
-      end
-
-      it "returns error 400 for suspicious limit" do
-        get "/tags/filter/search.json", params: { q: "", limit: "1; SELECT 1" }
-
-        expect(response.status).to eq(400)
-        expect(response.parsed_body["errors"].first).to eq(
-          I18n.t("invalid_params", message: "limit"),
-        )
+      describe "when limit params is invalid" do
+        include_examples "invalid limit params",
+                         "/tags/filter/search.json",
+                         SiteSetting.max_tag_search_results
       end
 
       it "includes required tag group information" do

programming_language: Ruby
filename: users_controller_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -4,7 +4,7 @@ require "rotp"
 
 RSpec.describe UsersController do
   fab!(:user) { Fabricate(:user) }
-  fab!(:user1) { Fabricate(:user) }
+  fab!(:user1) { Fabricate(:user, username: "someusername") }
   fab!(:another_user) { Fabricate(:user) }
   fab!(:invitee) { Fabricate(:user) }
   fab!(:inviter) { Fabricate(:user) }
@@ -4886,11 +4886,10 @@ RSpec.describe UsersController do
       expect(response.status).to eq(200)
     end
 
-    context "with limit" do
-      it "returns an error if value is invalid" do
-        get "/u/search/users.json", params: { limit: "-1" }
-        expect(response.status).to eq(400)
-      end
+    describe "when limit params is invalid" do
+      include_examples "invalid limit params",
+                       "/u/search/users.json",
+                       described_class::SEARCH_USERS_LIMIT
     end
 
     context "when `enable_names` is true" do
@@ -6185,6 +6184,14 @@ RSpec.describe UsersController do
       expect(response.status).to eq(200)
       expect(response.parsed_body["bookmarks"]).to eq([])
     end
+
+    describe "when limit params is invalid" do
+      before { sign_in(user1) }
+
+      include_examples "invalid limit params",
+                       "/u/someusername/bookmarks.json",
+                       described_class::BOOKMARKS_LIMIT
+    end
   end
 
   describe "#bookmarks excerpts" do

programming_language: Ruby
filename: user_bookmark_list_serializer_spec.rb
change_type: ModificationType.MODIFY
diff: @@ -18,7 +18,7 @@ RSpec.describe UserBookmarkListSerializer do
     let(:user_bookmark) { Fabricate(:bookmark, user: user, bookmarkable: Fabricate(:user)) }
 
     def run_serializer
-      bookmark_list = UserBookmarkList.new(user: user, guardian: Guardian.new(user), params: {})
+      bookmark_list = UserBookmarkList.new(user: user, guardian: Guardian.new(user))
       bookmark_list.load
       UserBookmarkListSerializer.new(bookmark_list)
     end

programming_language: Ruby