CVE-2022-23837: Allocation of Resources Without Limits or Throttling (CWE-770)
value: In api.rb in Sidekiq before 5.2.10 and 6.4.0, there is no limit on the number of days when requesting stats for the graph. This overloads the system, affecting the Web UI, and makes it unavailable to users.
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: 2022-01-21T21:15Z
Repository: https://github.com/mperham/sidekiq
Language: Ruby
Files changed: lib/sidekiq/api.rb, lib/sidekiq/web/application.rb, test/test_api.rb, test/test_web.rb
Commit date: 2022-01-20 10:42:26 -0800
Commit message: Validate `days` parameter to avoid possible DoS in Web UI

Thank you to Sergey Shpakov of http://tutum.space for reporting.
Diff: diff --git a/lib/sidekiq/api.rb b/lib/sidekiq/api.rb
index 47370cd4..89ffac9e 100644
--- a/lib/sidekiq/api.rb
+++ b/lib/sidekiq/api.rb
@@ -159,10 +159,12 @@ module Sidekiq
       end
     end
 
     class History
       def initialize(days_previous, start_date = nil)
+        # we only store five years of data in Redis
+        raise ArgumentError if days_previous < 1 || days_previous > (5 * 365)
         @days_previous = days_previous
         @start_date = start_date || Time.now.utc.to_date
       end
 
       def processed
diff --git a/lib/sidekiq/web/application.rb b/lib/sidekiq/web/application.rb
index 0a6db6f6..8debac91 100644
--- a/lib/sidekiq/web/application.rb
+++ b/lib/sidekiq/web/application.rb
@@ -48,11 +48,14 @@ module Sidekiq
       ""
     end
 
     get "/" do
       @redis_info = redis_info.select { |k, v| REDIS_KEYS.include? k }
-      stats_history = Sidekiq::Stats::History.new((params["days"] || 30).to_i)
+      days = (params["days"] || 30).to_i
+      return halt(401) if days < 1 || days > 180
+
+      stats_history = Sidekiq::Stats::History.new(days)
       @processed_history = stats_history.processed
       @failed_history = stats_history.failed
 
       erb(:dashboard)
     end
diff --git a/test/test_api.rb b/test/test_api.rb
index d92da85c..65a1e8bc 100644
--- a/test/test_api.rb
+++ b/test/test_api.rb
@@ -154,10 +154,19 @@ describe 'API' do
 
       after do
         Time::DATE_FORMATS[:default] = @before
       end
 
+      describe "history" do
+        it "does not allow invalid input" do
+          assert_raises(ArgumentError) { Sidekiq::Stats::History.new(-1) }
+          assert_raises(ArgumentError) { Sidekiq::Stats::History.new(0) }
+          assert_raises(ArgumentError) { Sidekiq::Stats::History.new(2000) }
+          assert Sidekiq::Stats::History.new(200)
+        end
+      end
+
       describe "processed" do
         it 'retrieves hash of dates' do
           Sidekiq.redis do |c|
             c.incrby("stat:processed:2012-12-24", 4)
             c.incrby("stat:processed:2012-12-25", 1)
diff --git a/test/test_web.rb b/test/test_web.rb
index 7b177660..d3c4b9a3 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -746,12 +746,13 @@ describe Sidekiq::Web do
 
     it 'authenticates successfuly' do
       basic_authorize 'a', 'b'
 
       get '/'
-
       assert_equal 200, last_response.status
+      get '/?days=1000000'
+      assert_equal 401, last_response.status
     end
   end
 
   describe 'custom session' do
     include Rack::Test::Methods

Vulnerable code:       stats_history = Sidekiq::Stats::History.new((params["days"] || 30).to_i)

Fixed code:         # we only store five years of data in Redis
        raise ArgumentError if days_previous < 1 || days_previous > (5 * 365)
      days = (params["days"] || 30).to_i
      return halt(401) if days < 1 || days > 180

      stats_history = Sidekiq::Stats::History.new(days)
      describe "history" do
        it "does not allow invalid input" do
          assert_raises(ArgumentError) { Sidekiq::Stats::History.new(-1) }
          assert_raises(ArgumentError) { Sidekiq::Stats::History.new(0) }
          assert_raises(ArgumentError) { Sidekiq::Stats::History.new(2000) }
          assert Sidekiq::Stats::History.new(200)
        end
      end

      get '/?days=1000000'
      assert_equal 401, last_response.status