CVE-2024-35231: Insufficient Information (NVD-CWE-noinfo)
value: rack-contrib provides contributed rack middleware and utilities for Rack, a Ruby web server interface. Versions of rack-contrib prior to 2.5.0 are vulnerable to denial of service due to the fact that the user controlled data `profiler_runs` was not constrained to any limitation. This would lead to allocating resources on the server side with no limitation and a potential denial of service by remotely user-controlled data. Version 2.5.0 contains a patch for the issue.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-27T17:15Z
Repository: https://github.com/rack/rack-contrib
Language: Ruby
Files changed: lib/rack/contrib/profiler.rb, test/spec_rack_profiler.rb
Commit date: 2024-05-24 09:48:48 +0900
Commit message: Specify a maximum number of runs to limit the request params -> DoS. (#191)


Diff: diff --git a/lib/rack/contrib/profiler.rb b/lib/rack/contrib/profiler.rb
index 9367720..324cd43 100644
--- a/lib/rack/contrib/profiler.rb
+++ b/lib/rack/contrib/profiler.rb
@@ -33,12 +33,15 @@ module Rack
     def initialize(app, options = {})
       @app = app
       @profile = nil
       @printer = parse_printer(options[:printer] || DEFAULT_PRINTER)
       @times = (options[:times] || 1).to_i
+      @maximum_runs = options.fetch(:maximum_runs, 10)
     end
 
+    attr :maximum_runs
+
     def call(env)
       if mode = profiling?(env)
         profile(env, mode)
       else
         @app.call(env)
@@ -59,18 +62,36 @@ module Rack
             false
           end
         end
       end
 
+      # How many times to run the request within the profiler.
+      # If the profiler_runs query parameter is set, use that.
+      # Otherwise, use the :times option passed to `#initialize`.
+      # If the profiler_runs query parameter is greater than the
+      # :maximum option passed to `#initialize`, use the :maximum
+      # option.
+      def runs(request)
+        if profiler_runs = request.params['profiler_runs']
+          profiler_runs = profiler_runs.to_i
+          if profiler_runs > @maximum_runs
+            return @maximum_runs
+          else
+            return profiler_runs
+          end
+        else
+          return @times
+        end
+      end
+
       def profile(env, mode)
         @profile = ::RubyProf::Profile.new(measure_mode: ::RubyProf.const_get(mode.upcase))
 
         GC.enable_stats if GC.respond_to?(:enable_stats)
         request = Rack::Request.new(env.clone)
-        runs = (request.params['profiler_runs'] || @times).to_i
         result = @profile.profile do
-          runs.times { @app.call(env) }
+          runs(request).times { @app.call(env) }
         end
         GC.disable_stats if GC.respond_to?(:disable_stats)
 
         [200, headers(@printer, env, mode), print(@printer, result)]
       end
diff --git a/test/spec_rack_profiler.rb b/test/spec_rack_profiler.rb
index d64853d..9feac63 100644
--- a/test/spec_rack_profiler.rb
+++ b/test/spec_rack_profiler.rb
@@ -25,10 +25,17 @@ begin
       req = Rack::MockRequest.env_for("/", :params => "profile=process_time&profiler_runs=#{runs}")
       body = profiler(app).call(req)[2]
       _(body.to_enum.to_a.join).must_match(/\[#{runs} calls, #{runs} total\]/)
     end
 
+    specify 'called more than the default maximum times via query params' do
+      runs = 20
+      req = Rack::MockRequest.env_for("/", :params => "profile=process_time&profiler_runs=#{runs}")
+      body = profiler(app).call(req)[2]
+      _(body.to_enum.to_a.join).must_match(/\[10 calls, 10 total\]/)
+    end
+
     specify 'CallStackPrinter has content-type test/html' do
       headers = profiler(app, :printer => :call_stack).call(request)[1]
       _(headers).must_equal "content-type"=>"text/html"
     end
 

Vulnerable code:         runs = (request.params['profiler_runs'] || @times).to_i
          runs.times { @app.call(env) }
Fixed code:       @maximum_runs = options.fetch(:maximum_runs, 10)
    attr :maximum_runs

      # How many times to run the request within the profiler.
      # If the profiler_runs query parameter is set, use that.
      # Otherwise, use the :times option passed to `#initialize`.
      # If the profiler_runs query parameter is greater than the
      # :maximum option passed to `#initialize`, use the :maximum
      # option.
      def runs(request)
        if profiler_runs = request.params['profiler_runs']
          profiler_runs = profiler_runs.to_i
          if profiler_runs > @maximum_runs
            return @maximum_runs
          else
            return profiler_runs
          end
        else
          return @times
        end
      end

          runs(request).times { @app.call(env) }
    specify 'called more than the default maximum times via query params' do
      runs = 20
      req = Rack::MockRequest.env_for("/", :params => "profile=process_time&profiler_runs=#{runs}")
      body = profiler(app).call(req)[2]
      _(body.to_enum.to_a.join).must_match(/\[10 calls, 10 total\]/)
    end