CVE-2023-38684: Allocation of Resources Without Limits or Throttling (CWE-770)
value: 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.
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: 7.5
Published: 2023-07-28T16:15Z
Repository: https://github.com/discourse/discourse
Language: Ruby
Files changed: app/controllers/admin/api_controller.rb, app/controllers/admin/reports_controller.rb, app/controllers/admin/staff_action_logs_controller.rb, app/controllers/application_controller.rb, app/controllers/directory_items_controller.rb, app/controllers/drafts_controller.rb, app/controllers/groups_controller.rb, app/controllers/notifications_controller.rb, app/controllers/post_action_users_controller.rb, app/controllers/posts_controller.rb, app/controllers/tag_groups_controller.rb, app/controllers/tags_controller.rb, app/controllers/users_controller.rb, app/models/user_bookmark_list.rb, lib/bookmark_query.rb, plugins/chat/app/controllers/chat/api/channels_memberships_controller.rb, spec/lib/bookmark_query_spec.rb, spec/models/user_bookmark_list_spec.rb, spec/rails_helper.rb, spec/requests/admin/api_controller_spec.rb, spec/requests/admin/reports_controller_spec.rb, spec/requests/admin/staff_action_logs_controller_spec.rb, spec/requests/directory_items_controller_spec.rb, spec/requests/drafts_controller_spec.rb, spec/requests/examples/invalid_limit_params.rb, spec/requests/notifications_controller_spec.rb, spec/requests/post_action_users_controller_spec.rb, spec/requests/posts_controller_spec.rb, spec/requests/tag_groups_controller_spec.rb, spec/requests/tags_controller_spec.rb, spec/requests/users_controller_spec.rb, spec/serializers/user_bookmark_list_serializer_spec.rb
Commit date: 2023-07-28 12:53:46 +0100
Commit message: 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.

Diff: diff --git a/app/controllers/admin/api_controller.rb b/app/controllers/admin/api_controller.rb
index 49b8a70414eb..6238287c0677 100644
--- a/app/controllers/admin/api_controller.rb
+++ b/app/controllers/admin/api_controller.rb
@@ -2,13 +2,15 @@
 
 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
         .where(hidden: false)
         .includes(:user, :api_key_scopes)
diff --git a/app/controllers/admin/reports_controller.rb b/app/controllers/admin/reports_controller.rb
index ffa8463a9c4d..ab49a29aa9de 100644
--- a/app/controllers/admin/reports_controller.rb
+++ b/app/controllers/admin/reports_controller.rb
@@ -1,8 +1,10 @@
 # frozen_string_literal: true
 
 class Admin::ReportsController < Admin::StaffController
+  REPORTS_LIMIT = 50
+
   def index
     reports_methods =
       ["page_view_total_reqs"] +
         ApplicationRequest
           .req_types
@@ -106,14 +108,11 @@ class Admin::ReportsController < Admin::StaffController
     end
 
     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)
 
     { start_date: start_date, end_date: end_date, filters: filters, facets: facets, limit: limit }
diff --git a/app/controllers/admin/staff_action_logs_controller.rb b/app/controllers/admin/staff_action_logs_controller.rb
index 69687294ca93..f419f47c5bfd 100644
--- a/app/controllers/admin/staff_action_logs_controller.rb
+++ b/app/controllers/admin/staff_action_logs_controller.rb
@@ -1,13 +1,15 @@
 # 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
     staff_action_logs = staff_action_logs.offset(page * page_size).limit(page_size).to_a
 
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index c6c074823436..79c652a3fe4a 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1082,6 +1082,24 @@ class ApplicationController < ActionController::Base
             weight: v[:weight],
           }
         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
diff --git a/app/controllers/directory_items_controller.rb b/app/controllers/directory_items_controller.rb
index 2fbfcf120482..7d70b1eeae7b 100644
--- a/app/controllers/directory_items_controller.rb
+++ b/app/controllers/directory_items_controller.rb
@@ -79,12 +79,11 @@ class DirectoryItemsController < ApplicationController
       else
         result = result.where("false")
       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
 
     more_params = params.slice(:period, :order, :asc, :group, :user_field_ids).permit!
diff --git a/app/controllers/drafts_controller.rb b/app/controllers/drafts_controller.rb
index 307104b9b4a6..ba767b8a5d83 100644
--- a/app/controllers/drafts_controller.rb
+++ b/app/controllers/drafts_controller.rb
@@ -3,15 +3,21 @@
 class DraftsController < ApplicationController
   requires_login
 
   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
 
   def show
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index f0e47f36c8bb..a3a267815f06 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -228,19 +228,20 @@ class GroupsController < ApplicationController
     @link = Discourse.base_url
     @description = I18n.t("rss_description.group_mentions", group_name: group.name)
     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"
     order = "NOT group_users.owner"
 
diff --git a/app/controllers/notifications_controller.rb b/app/controllers/notifications_controller.rb
index d5fadbc49427..75cd9c60b797 100644
--- a/app/controllers/notifications_controller.rb
+++ b/app/controllers/notifications_controller.rb
@@ -3,10 +3,12 @@
 class NotificationsController < ApplicationController
   requires_login
   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]
         user_record = User.find_by(username: params[:username].to_s)
         raise Discourse::NotFound if !user_record
@@ -23,12 +25,11 @@ class NotificationsController < ApplicationController
           (raise Discourse::InvalidParameters.new("invalid notification type: #{type}"))
       end
     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
 
       notifications =
         Notification.prioritized_list(current_user, count: limit, types: notification_types)
diff --git a/app/controllers/post_action_users_controller.rb b/app/controllers/post_action_users_controller.rb
index 7b39cd2fabb2..36e87734809e 100644
--- a/app/controllers/post_action_users_controller.rb
+++ b/app/controllers/post_action_users_controller.rb
@@ -1,15 +1,17 @@
 # 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
     guardian.ensure_can_see!(post)
 
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index 260d08f9dc06..0240cf0791ad 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -660,17 +660,19 @@ class PostsController < ApplicationController
     post.unhide!
 
     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)
 
     render_serialized(posts, AdminUserActionSerializer)
   end
diff --git a/app/controllers/tag_groups_controller.rb b/app/controllers/tag_groups_controller.rb
index fd6dae701d3f..74aa7528af0e 100644
--- a/app/controllers/tag_groups_controller.rb
+++ b/app/controllers/tag_groups_controller.rb
@@ -77,11 +77,14 @@ class TagGroupsController < ApplicationController
 
     if params[:names].present?
       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:
                matches.map { |x| { name: x.name, tag_names: x.tags.base_tags.pluck(:name).sort } },
            }
diff --git a/app/controllers/tags_controller.rb b/app/controllers/tags_controller.rb
index f2df3fe97556..dae4b9790eca 100644
--- a/app/controllers/tags_controller.rb
+++ b/app/controllers/tags_controller.rb
@@ -290,17 +290,12 @@ class TagsController < ::ApplicationController
       selected_tags: params[:selected_tags],
       exclude_synonyms: params[:excludeSynonyms],
       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]
 
     if !params[:q].blank?
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 29063caae6b0..92ed0207429d 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -1180,10 +1180,12 @@ class UsersController < ApplicationController
       EmailToken.enqueue_signup_email(@email_token, to_address: @user.email)
       render body: nil
     end
   end
 
+  SEARCH_USERS_LIMIT = 50
+
   def search_users
     term = params[:term].to_s.strip
 
     topic_id = params[:topic_id].to_i if params[:topic_id].present?
     category_id = params[:category_id].to_i if params[:category_id].present?
@@ -1202,14 +1204,15 @@ class UsersController < ApplicationController
 
     options[:include_staged_users] = !!ActiveModel::Type::Boolean.new.cast(
       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
 
     results = UserSearch.new(term, options).search
     to_render = serialize_found_users(results)
@@ -1731,18 +1734,28 @@ class UsersController < ApplicationController
     guardian.ensure_can_edit!(user)
     user.user_profile.update(featured_topic_id: nil)
     render json: success_json
   end
 
+  BOOKMARKS_LIMIT = 20
+
   def bookmarks
     user = fetch_user_from_params
     guardian.ensure_can_edit!(user)
     user_guardian = Guardian.new(user)
 
     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?
           render json: { bookmarks: [] }
         else
@@ -1787,14 +1800,13 @@ class UsersController < ApplicationController
 
       bookmark_list =
         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)
         end
       end
diff --git a/app/models/user_bookmark_list.rb b/app/models/user_bookmark_list.rb
index a55a72a81418..a085a5b3ec7b 100644
--- a/app/models/user_bookmark_list.rb
+++ b/app/models/user_bookmark_list.rb
@@ -6,28 +6,34 @@ class UserBookmarkList
   PER_PAGE = 20
 
   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
diff --git a/lib/bookmark_query.rb b/lib/bookmark_query.rb
index 3a54a1958076..7624f0113691 100644
--- a/lib/bookmark_query.rb
+++ b/lib/bookmark_query.rb
@@ -24,23 +24,22 @@ class BookmarkQuery
     end
   end
 
   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
         .registered_bookmarkables
         .map do |bookmarkable|
@@ -49,11 +48,11 @@ class BookmarkQuery
           # this could occur if there is some security reason that the user cannot
           # access the bookmarkables that they have bookmarked, e.g. if they had 1 bookmark
           # 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
 
           # this is purely to make the query easy to read and debug, otherwise it's
@@ -76,16 +75,17 @@ class BookmarkQuery
         bookmarks.updated_at DESC",
       )
 
     @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
 end
diff --git a/plugins/chat/app/controllers/chat/api/channels_memberships_controller.rb b/plugins/chat/app/controllers/chat/api/channels_memberships_controller.rb
index 0392b3bb1771..d35d63df59cb 100644
--- a/plugins/chat/app/controllers/chat/api/channels_memberships_controller.rb
+++ b/plugins/chat/app/controllers/chat/api/channels_memberships_controller.rb
@@ -1,13 +1,15 @@
 # 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(
         channel: channel_from_params,
         offset: offset,
diff --git a/spec/lib/bookmark_query_spec.rb b/spec/lib/bookmark_query_spec.rb
index 3ce8ad947112..d54c10d07d25 100644
--- a/spec/lib/bookmark_query_spec.rb
+++ b/spec/lib/bookmark_query_spec.rb
@@ -2,14 +2,13 @@
 
 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
     before do
       register_test_bookmarkable
@@ -65,11 +64,11 @@ RSpec.describe BookmarkQuery do
       UserTestBookmarkable.expects(:list_query).returns(nil)
       bookmarks = bookmark_query.list_all
       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,
           raw: "Some post content here",
           topic: Fabricate(:topic, title: "Bugfix game for devs"),
@@ -87,21 +86,21 @@ RSpec.describe BookmarkQuery do
         Fabricate(:topic_user, user: user, topic: bookmark3.bookmarkable.topic)
         Fabricate(:topic_user, user: user, topic: bookmark4.bookmarkable.topic)
       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
 
       context "with custom bookmarkable fitering" do
         before { register_test_bookmarkable }
@@ -109,11 +108,11 @@ RSpec.describe BookmarkQuery do
         let!(:bookmark5) do
           Fabricate(:bookmark, user: user, bookmarkable: Fabricate(:user, username: "bookmarkking"))
         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
     end
 
@@ -203,14 +202,13 @@ RSpec.describe BookmarkQuery do
         GroupUser.create(user: user, group: group)
         expect(bookmark_query.list_all.map(&:id)).to include(post_bookmark.id)
       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
 
   describe "#list_all ordering" do
diff --git a/spec/models/user_bookmark_list_spec.rb b/spec/models/user_bookmark_list_spec.rb
index 1876ef3f827d..c6074caff63b 100644
--- a/spec/models/user_bookmark_list_spec.rb
+++ b/spec/models/user_bookmark_list_spec.rb
@@ -1,11 +1,10 @@
 # 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
 
     Fabricate(:topic_user, user: user, topic: post_bookmark.bookmarkable.topic)
@@ -19,27 +18,30 @@ RSpec.describe UserBookmarkList do
   let(:topic_bookmark) { Fabricate(:bookmark, user: user, bookmarkable: Fabricate(:topic)) }
   let(:user_bookmark) { Fabricate(:bookmark, user: user, bookmarkable: Fabricate(:user)) }
 
   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
 
   it "defaults to 20 per page" do
     expect(list.per_page).to eq(20)
   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
 end
diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb
index 63e010de110a..2d0550dd8f27 100644
--- a/spec/rails_helper.rb
+++ b/spec/rails_helper.rb
@@ -76,10 +76,11 @@ Shoulda::Matchers.configure do |config|
 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 }
 Dir[Rails.root.join("spec/system/page_objects/**/*.rb")].each { |f| require f }
 
diff --git a/spec/requests/admin/api_controller_spec.rb b/spec/requests/admin/api_controller_spec.rb
index d83520968f56..b582558fce9b 100644
--- a/spec/requests/admin/api_controller_spec.rb
+++ b/spec/requests/admin/api_controller_spec.rb
@@ -18,10 +18,16 @@ RSpec.describe Admin::ApiController do
 
         expect(response.status).to eq(200)
         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"
 
         expect(response.status).to eq(200)
         expect(response.parsed_body["keys"].map { |x| x["id"] }).to contain_exactly(
diff --git a/spec/requests/admin/reports_controller_spec.rb b/spec/requests/admin/reports_controller_spec.rb
index 5494fc230ac4..c300947e1bfa 100644
--- a/spec/requests/admin/reports_controller_spec.rb
+++ b/spec/requests/admin/reports_controller_spec.rb
@@ -28,10 +28,16 @@ RSpec.describe Admin::ReportsController do
           expect(response.parsed_body["reports"].count).to eq(2)
         end
       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",
                 params: {
                   reports: {
@@ -152,10 +158,16 @@ RSpec.describe Admin::ReportsController do
 
             expect(response.status).to eq(200)
             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
         fab!(:category) { Fabricate(:category) }
         fab!(:topic) { Fabricate(:topic, category: category) }
diff --git a/spec/requests/admin/staff_action_logs_controller_spec.rb b/spec/requests/admin/staff_action_logs_controller_spec.rb
index 93a8bf1fd925..4724124869e4 100644
--- a/spec/requests/admin/staff_action_logs_controller_spec.rb
+++ b/spec/requests/admin/staff_action_logs_controller_spec.rb
@@ -25,10 +25,16 @@ RSpec.describe Admin::StaffActionLogsController do
         expect(json["extras"]["user_history_actions"]).to include(
           "id" => "delete_topic",
           "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
       before { sign_in(admin) }
 
diff --git a/spec/requests/directory_items_controller_spec.rb b/spec/requests/directory_items_controller_spec.rb
index 6576072fcc41..94eaec500b8d 100644
--- a/spec/requests/directory_items_controller_spec.rb
+++ b/spec/requests/directory_items_controller_spec.rb
@@ -28,26 +28,11 @@ RSpec.describe DirectoryItemsController do
       json = response.parsed_body
 
       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
     before { DirectoryItem.refresh! }
 
diff --git a/spec/requests/drafts_controller_spec.rb b/spec/requests/drafts_controller_spec.rb
index 7b0d7f03b80f..1d88db0b4a51 100644
--- a/spec/requests/drafts_controller_spec.rb
+++ b/spec/requests/drafts_controller_spec.rb
@@ -1,25 +1,33 @@
 # 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)
       parsed = response.parsed_body
       expect(response.parsed_body["drafts"].length).to eq(1)
     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"
       expect(response.status).to eq(200)
       expect(response.parsed_body["drafts"].length).to eq(0)
@@ -44,11 +52,11 @@ RSpec.describe DraftsController do
     end
   end
 
   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"
       expect(response.status).to eq(200)
       expect(response.parsed_body["draft"]).to eq("test")
@@ -60,11 +68,11 @@ RSpec.describe DraftsController do
       post "/drafts.json"
       expect(response.status).to eq(403)
     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 }
 
       expect(response.status).to eq(200)
       expect(Draft.get(user, "xyz", 0)).to eq(%q({"my":"data"}))
@@ -75,11 +83,11 @@ RSpec.describe DraftsController do
       post "/drafts.json", params: { data: { my: "data" }.to_json, sequence: 0 }
       expect(response.status).to eq(404)
     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",
            params: {
              draft_key: "topic",
@@ -101,11 +109,11 @@ RSpec.describe DraftsController do
       expect(response.parsed_body["conflict_user"]["id"]).to eq(post.last_editor.id)
       expect(response.parsed_body["conflict_user"]).to include("avatar_template")
     end
 
     it "cant trivially resolve conflicts without interaction" do
-      user = sign_in(Fabricate(:user))
+      sign_in(user)
 
       DraftSequence.next!(user, "abc")
 
       post "/drafts.json",
            params: {
@@ -118,11 +126,11 @@ RSpec.describe DraftsController do
       expect(response.status).to eq(200)
       expect(response.parsed_body["draft_sequence"]).to eq(1)
     end
 
     it "has a clean protocol for ownership handover" do
-      user = sign_in(Fabricate(:user))
+      sign_in(user)
 
       post "/drafts.json",
            params: {
              draft_key: "abc",
              sequence: 0,
@@ -168,11 +176,11 @@ RSpec.describe DraftsController do
       expect(response.status).to eq(200)
       expect(response.parsed_body["draft_sequence"]).to eq(3)
     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)
 
       post "/drafts.json",
            params: {
@@ -228,11 +236,11 @@ RSpec.describe DraftsController do
     end
   end
 
   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 }
 
       expect(response.status).to eq(200)
       expect(Draft.get(user, "xxx", 0)).to eq(nil)
diff --git a/spec/requests/examples/invalid_limit_params.rb b/spec/requests/examples/invalid_limit_params.rb
new file mode 100644
index 000000000000..9944f6acbd3e
--- /dev/null
+++ b/spec/requests/examples/invalid_limit_params.rb
@@ -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
diff --git a/spec/requests/notifications_controller_spec.rb b/spec/requests/notifications_controller_spec.rb
index b9ae086d61ea..c940bf129b67 100644
--- a/spec/requests/notifications_controller_spec.rb
+++ b/spec/requests/notifications_controller_spec.rb
@@ -85,10 +85,19 @@ RSpec.describe NotificationsController do
           expect(user.reload.total_unread_notifications).to eq(1)
         ensure
           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)
           put "/notifications/mark-read.json", params: { id: notification.id }
           expect(response.status).to eq(200)
diff --git a/spec/requests/post_action_users_controller_spec.rb b/spec/requests/post_action_users_controller_spec.rb
index 2de40facfb16..d75bbaed102c 100644
--- a/spec/requests/post_action_users_controller_spec.rb
+++ b/spec/requests/post_action_users_controller_spec.rb
@@ -2,10 +2,18 @@
 
 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]
 
       PostActionCreator.new(
diff --git a/spec/requests/posts_controller_spec.rb b/spec/requests/posts_controller_spec.rb
index 4fef6b7fce2a..c641f1b2dd46 100644
--- a/spec/requests/posts_controller_spec.rb
+++ b/spec/requests/posts_controller_spec.rb
@@ -2252,10 +2252,18 @@ RSpec.describe PostsController do
         sign_in(user)
         get "/posts/system/deleted.json"
         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"
         expect(response.status).to eq(200)
       end
diff --git a/spec/requests/tag_groups_controller_spec.rb b/spec/requests/tag_groups_controller_spec.rb
index 87b49faa37dd..2c30910e899b 100644
--- a/spec/requests/tag_groups_controller_spec.rb
+++ b/spec/requests/tag_groups_controller_spec.rb
@@ -46,10 +46,16 @@ RSpec.describe TagGroupsController do
     let(:staff) { Group::AUTO_GROUPS[:staff] }
 
     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)
         tag_group2 = tag_group_with_permission(everyone, readonly)
 
diff --git a/spec/requests/tags_controller_spec.rb b/spec/requests/tags_controller_spec.rb
index 8f96113e3a7b..5a054498dee2 100644
--- a/spec/requests/tags_controller_spec.rb
+++ b/spec/requests/tags_controller_spec.rb
@@ -1101,26 +1101,14 @@ RSpec.describe TagsController do
           response.parsed_body["results"].map { |j| j["id"] },
           %w[common1 common2 group1tag group1tag2],
         )
       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
         tag1 = Fabricate(:tag)
         tag2 = Fabricate(:tag)
diff --git a/spec/requests/users_controller_spec.rb b/spec/requests/users_controller_spec.rb
index 1c9729df0a93..605588bd0638 100644
--- a/spec/requests/users_controller_spec.rb
+++ b/spec/requests/users_controller_spec.rb
@@ -2,11 +2,11 @@
 
 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) }
 
   fab!(:admin) { Fabricate(:admin) }
@@ -4884,15 +4884,14 @@ RSpec.describe UsersController do
       sign_in(pm_topic.user)
       get "/u/search/users.json", params: { term: "", topic_id: pm_topic.id, category_id: "" }
       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
       before { SiteSetting.enable_names = true }
 
@@ -6183,10 +6182,18 @@ RSpec.describe UsersController do
       sign_in(user1)
       get "/u/#{user1.username}/bookmarks.json", params: { q: "badsearch" }
       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
     fab!(:user) { Fabricate(:user) }
     let!(:topic) { Fabricate(:topic, user: user) }
diff --git a/spec/serializers/user_bookmark_list_serializer_spec.rb b/spec/serializers/user_bookmark_list_serializer_spec.rb
index 6c5934ccb257..2ec484cb9dc3 100644
--- a/spec/serializers/user_bookmark_list_serializer_spec.rb
+++ b/spec/serializers/user_bookmark_list_serializer_spec.rb
@@ -16,11 +16,11 @@ RSpec.describe UserBookmarkListSerializer do
     let(:post_bookmark) { Fabricate(:bookmark, user: user, bookmarkable: Fabricate(:post)) }
     let(:topic_bookmark) { Fabricate(:bookmark, user: user, bookmarkable: Fabricate(:topic)) }
     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
 
     it "chooses the correct class of serializer for all the bookmarkable types" do

Vulnerable code:     limit = (params[:limit] || 50).to_i.clamp(1, 50)
    limit = nil
    if report_params.has_key?(:limit) && report_params[:limit].to_i > 0
      limit = report_params[:limit].to_i
    end
    page_size = (params[:limit] || 200).to_i.clamp(1, 200)
    limit = [params[:limit].to_i, PAGE_SIZE].min if params[:limit].to_i > 0
    limit ||= PAGE_SIZE
    params.permit(:limit)
    stream = Draft.stream(user: current_user, offset: params[:offset], limit: params[:limit])
    limit = (params[:limit] || 50).to_i
    raise Discourse::InvalidParameters.new(:limit) if limit < 0 || limit > 1000
      limit = (params[:limit] || 15).to_i
      limit = 50 if limit > 50
    page_size = (params[:limit] || 200).to_i
    limit = [(params[:limit] || 60).to_i, 100].min
    matches = matches.order("name").limit(params[:limit] || 5)
    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 params[:limit].present?
      options[:limit] = params[:limit].to_i
      raise Discourse::InvalidParameters.new(:limit) if options[:limit] <= 0
        bookmark_list = UserBookmarkList.new(user: user, guardian: guardian, params: params)
          params: {
            per_page: USER_MENU_LIST_LIMIT - reminder_notifications.size,
          },
  def initialize(user:, guardian:, params:)
    @params = params
    @params.merge!(per_page: PER_PAGE) if params[:per_page].blank?
    @params[:per_page] = PER_PAGE if @params[:per_page] > PER_PAGE
    query = BookmarkQuery.new(user: @user, guardian: @guardian, params: @params)
    @has_more = (@params[:page].to_i + 1) * @params[:per_page] < query.count

  def per_page
    @per_page ||= @params[:per_page]
  end
  def initialize(user:, guardian: nil, params: {})
    @params = params
    @page = @params[:page].to_i
    @limit = @params[:limit].present? ? @params[:limit].to_i : @params[:per_page]
    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
          if search_term.present?
    results = results.offset(@page * @params[:per_page]) if @page.positive?
    results = results.limit(@limit).to_a
    limit = (params[:limit] || 50).to_i.clamp(1, 50)
  let(:params) { {} }
  def bookmark_query(user: nil, params: nil)
    BookmarkQuery.new(user: user || self.user, params: params || self.params)
    context "when q param is provided" do
        bookmarks = bookmark_query(params: { q: "check" }).list_all
        bookmarks = bookmark_query(params: { q: "content" }).list_all
        bookmarks = bookmark_query(params: { q: "bugfix" }).list_all
          bookmarks = bookmark_query(params: { q: "bookmarkk" }).list_all
    context "when the limit param is provided" do
      let(:params) { { limit: 1 } }
        expect(bookmark_query.list_all.count).to eq(1)
  let(:params) { {} }
  let(:list) { UserBookmarkList.new(user: user, guardian: Guardian.new(user), params: params) }
    let(:params) { { per_page: 1000 } }

    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
      user = sign_in(Fabricate(:user))
      user = sign_in(Fabricate(:user))
      user = sign_in(Fabricate(:user))
      user = sign_in(Fabricate(:user))
      user = sign_in(Fabricate(:user))
      user = sign_in(Fabricate(:user))
      user = sign_in(Fabricate(:user))
      user = sign_in(Fabricate(:user))
      user = sign_in(Fabricate(:user))
      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"),
        )
  fab!(:user1) { Fabricate(:user) }
    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
      bookmark_list = UserBookmarkList.new(user: user, guardian: Guardian.new(user), params: {})
Fixed code:   INDEX_LIMIT = 50

    limit = fetch_limit_from_params(default: INDEX_LIMIT, max: INDEX_LIMIT)
  REPORTS_LIMIT = 50

    limit = fetch_limit_from_params(params: report_params, default: nil, max: REPORTS_LIMIT)
  INDEX_LIMIT = 200

    page_size = fetch_limit_from_params(default: INDEX_LIMIT, max: INDEX_LIMIT)

  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
    limit = fetch_limit_from_params(default: PAGE_SIZE, max: PAGE_SIZE)
  INDEX_LIMIT = 50

    stream =
      Draft.stream(
        user: current_user,
        offset: params[:offset],
        limit: fetch_limit_from_params(default: nil, max: INDEX_LIMIT),
      )
  MEMBERS_LIMIT = 1_000

    limit = fetch_limit_from_params(default: 50, max: MEMBERS_LIMIT)
  INDEX_LIMIT = 50

      limit = fetch_limit_from_params(default: 15, max: INDEX_LIMIT)
  INDEX_LIMIT = 200

    page_size = fetch_limit_from_params(default: INDEX_LIMIT, max: INDEX_LIMIT)
  DELETED_POSTS_MAX_LIMIT = 100

    limit = fetch_limit_from_params(default: 60, max: DELETED_POSTS_MAX_LIMIT)
    matches =
      matches.order("name").limit(
        fetch_limit_from_params(default: 5, max: SiteSetting.max_tag_search_results),
      )
    if limit = fetch_limit_from_params(default: nil, max: SiteSetting.max_tag_search_results)
      filter_params[:limit] = limit
  SEARCH_USERS_LIMIT = 50


    if limit = fetch_limit_from_params(default: nil, max: SEARCH_USERS_LIMIT)
      options[:limit] = limit

  BOOKMARKS_LIMIT = 20

        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),
          )

          per_page: USER_MENU_LIST_LIMIT - reminder_notifications.size,

  def initialize(user:, guardian:, search_term: nil, per_page: nil, page: 0)
    @per_page = per_page || PER_PAGE
    @per_page = PER_PAGE if @per_page > PER_PAGE

    @search_term = search_term
    @page = page.to_i
    query =
      BookmarkQuery.new(
        user: @user,
        guardian: @guardian,
        search_term: @search_term,
        page: @page,
        per_page: @per_page,
      )

    @has_more = (@page.to_i + 1) * @per_page < query.count
  def initialize(user:, guardian: nil, search_term: nil, page: nil, per_page: nil)
    @search_term = search_term
    @page = page ? page.to_i : 0
    @per_page = per_page ? per_page.to_i : 20
    ts_query = @search_term.present? ? Search.ts_query(term: @search_term) : nil
    search_term_wildcard = @search_term.present? ? "%#{@search_term}%" : nil
          if @search_term.present?
    results = results.offset(@page * @per_page) if @page.positive?
    results = results.limit(@per_page).to_a

  INDEX_LIMIT = 50

    limit = fetch_limit_from_params(default: INDEX_LIMIT, max: INDEX_LIMIT)
  def bookmark_query(user: nil, search_term: nil, per_page: nil)
    BookmarkQuery.new(user: user || self.user, search_term:, per_page:)
    context "when search_term is provided" do
        bookmarks = bookmark_query(search_term: "check").list_all
        bookmarks = bookmark_query(search_term: "content").list_all
        bookmarks = bookmark_query(search_term: "bugfix").list_all
          bookmarks = bookmark_query(search_term: "bookmarkk").list_all
    context "when the per_page is provided" do
        expect(bookmark_query(per_page: 1).list_all.count).to eq(1)
  let(:list) { UserBookmarkList.new(user: user, guardian: Guardian.new(user)) }



      list = UserBookmarkList.new(user: user, guardian: Guardian.new(user), per_page: 1000)

Dir[Rails.root.join("spec/requests/examples/*.rb")].each { |f| require f }
      describe "when limit params is invalid" do
        include_examples "invalid limit params",
                         "/admin/api/keys.json",
                         described_class::INDEX_LIMIT
      end

        context "when limit param is invalid" do
          include_examples "invalid limit params",
                           "/admin/reports/topics.json",
                           described_class::REPORTS_LIMIT
        end


        context "when limit param is invalid" do
          include_examples "invalid limit params",
                           "/admin/reports/topics.json",
                           described_class::REPORTS_LIMIT
        end

      describe "when limit params is invalid" do
        include_examples "invalid limit params",
                         "/admin/logs/staff_action_logs.json",
                         described_class::INDEX_LIMIT
      end
    include_examples "invalid limit params", "/directory_items.json", described_class::PAGE_SIZE
  fab!(:user) { Fabricate(:user) }

    describe "when limit params is invalid" do
      before { sign_in(user) }

      include_examples "invalid limit params", "/drafts.json", described_class::INDEX_LIMIT
    end

      sign_in(user)
      sign_in(user)
      sign_in(user)
      sign_in(user)
      sign_in(user)
      sign_in(user)
      sign_in(user)
      sign_in(user)
      sign_in(user)
# 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
        describe "when limit params is invalid" do
          include_examples "invalid limit params",
                           "/notifications.json",
                           described_class::INDEX_LIMIT,
                           params: {
                             recent: true,
                           }
        end

  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

      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

    describe "when limit params is invalid" do
      include_examples "invalid limit params",
                       "/tag_groups/filter/search.json",
                       SiteSetting.max_tag_search_results
    end

      describe "when limit params is invalid" do
        include_examples "invalid limit params",
                         "/tags/filter/search.json",
                         SiteSetting.max_tag_search_results
  fab!(:user1) { Fabricate(:user, username: "someusername") }
    describe "when limit params is invalid" do
      include_examples "invalid limit params",
                       "/u/search/users.json",
                       described_class::SEARCH_USERS_LIMIT

    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
      bookmark_list = UserBookmarkList.new(user: user, guardian: Guardian.new(user))