CVE-2024-25122: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting') (CWE-79)
value: sidekiq-unique-jobs is an open source project which prevents simultaneous Sidekiq jobs with the same unique arguments to run. Specially crafted GET request parameters handled by any of the following endpoints of sidekiq-unique-jobs' "admin" web UI, allow a super-user attacker, or an unwitting, but authorized, victim, who has received a disguised / crafted link, to successfully execute malicious code, which could potentially steal cookies, session data, or local storage data from the app the sidekiq-unique-jobs web UI is mounted in. 1. `/changelogs`, 2. `/locks` or 3. `/expiring_locks`. This issue has been addressed in versions 7.1.33 and 8.0.7. Users are advised to upgrade. There are no known workarounds for this vulnerability.

CWE description: The product does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.
Published: 2024-02-13T19:15Z
Repository: https://github.com/mhenrixon/sidekiq-unique-jobs
Language: Ruby
Files changed: lib/sidekiq_unique_jobs/web.rb
Commit date: 2024-02-05 17:15:06 +0200
Commit message: fix(xss): sanitize parameters (#829)


Diff: diff --git a/lib/sidekiq_unique_jobs/web.rb b/lib/sidekiq_unique_jobs/web.rb
index ed68a57..bf515e8 100644
--- a/lib/sidekiq_unique_jobs/web.rb
+++ b/lib/sidekiq_unique_jobs/web.rb
@@ -11,15 +11,15 @@ module SidekiqUniqueJobs
       app.helpers do
         include Web::Helpers
       end
 
       app.get "/changelogs" do
-        @filter         = params[:filter] || "*"
+        @filter         = h(params[:filter] || "*")
         @filter         = "*" if @filter == ""
-        @count          = (params[:count] || 100).to_i
-        @current_cursor = params[:cursor].to_i
-        @prev_cursor    = params[:prev_cursor].to_i
+        @count          = h(params[:count] || 100).to_i
+        @current_cursor = h(params[:cursor]).to_i
+        @prev_cursor    = h(params[:prev_cursor]).to_i
         @total_size, @next_cursor, @changelogs = changelog.page(
           cursor: @current_cursor,
           pattern: @filter,
           page_size: @count,
         )
@@ -31,15 +31,15 @@ module SidekiqUniqueJobs
         changelog.clear
         redirect_to :changelogs
       end
 
       app.get "/locks" do
-        @filter         = params[:filter] || "*"
+        @filter         = h(params[:filter]) || "*"
         @filter         = "*" if @filter == ""
-        @count          = (params[:count] || 100).to_i
-        @current_cursor = params[:cursor].to_i
-        @prev_cursor    = params[:prev_cursor].to_i
+        @count          = h(params[:count] || 100).to_i
+        @current_cursor = h(params[:cursor]).to_i
+        @prev_cursor    = h(params[:prev_cursor]).to_i
 
         @total_size, @next_cursor, @locks = digests.page(
           cursor: @current_cursor,
           pattern: @filter,
           page_size: @count,
@@ -47,15 +47,15 @@ module SidekiqUniqueJobs
 
         erb(unique_template(:locks))
       end
 
       app.get "/expiring_locks" do
-        @filter         = params[:filter] || "*"
+        @filter         = h(params[:filter]) || "*"
         @filter         = "*" if @filter == ""
-        @count          = (params[:count] || 100).to_i
-        @current_cursor = params[:cursor].to_i
-        @prev_cursor    = params[:prev_cursor].to_i
+        @count          = h(params[:count] || 100).to_i
+        @current_cursor = h(params[:cursor]).to_i
+        @prev_cursor    = h(params[:prev_cursor]).to_i
 
         @total_size, @next_cursor, @locks = expiring_digests.page(
           cursor: @current_cursor,
           pattern: @filter,
           page_size: @count,
@@ -69,24 +69,24 @@ module SidekiqUniqueJobs
         expiring_digests.delete_by_pattern("*", count: digests.count)
         redirect_to :locks
       end
 
       app.get "/locks/:digest" do
-        @digest = params[:digest]
+        @digest = h(params[:digest])
         @lock   = SidekiqUniqueJobs::Lock.new(@digest)
 
         erb(unique_template(:lock))
       end
 
       app.get "/locks/:digest/delete" do
-        digests.delete_by_digest(params[:digest])
-        expiring_digests.delete_by_digest(params[:digest])
+        digests.delete_by_digest(h(params[:digest]))
+        expiring_digests.delete_by_digest(h(params[:digest]))
         redirect_to :locks
       end
 
       app.get "/locks/:digest/jobs/:job_id/delete" do
-        @digest = params[:digest]
+        @digest = h(params[:digest])
         @lock   = SidekiqUniqueJobs::Lock.new(@digest)
         @lock.unlock(params[:job_id])
 
         redirect_to "locks/#{@lock.key}"
       end

Vulnerable code:         @filter         = params[:filter] || "*"
        @count          = (params[:count] || 100).to_i
        @current_cursor = params[:cursor].to_i
        @prev_cursor    = params[:prev_cursor].to_i
        @filter         = params[:filter] || "*"
        @count          = (params[:count] || 100).to_i
        @current_cursor = params[:cursor].to_i
        @prev_cursor    = params[:prev_cursor].to_i
        @filter         = params[:filter] || "*"
        @count          = (params[:count] || 100).to_i
        @current_cursor = params[:cursor].to_i
        @prev_cursor    = params[:prev_cursor].to_i
        @digest = params[:digest]
        digests.delete_by_digest(params[:digest])
        expiring_digests.delete_by_digest(params[:digest])
        @digest = params[:digest]
Fixed code:         @filter         = h(params[:filter] || "*")
        @count          = h(params[:count] || 100).to_i
        @current_cursor = h(params[:cursor]).to_i
        @prev_cursor    = h(params[:prev_cursor]).to_i
        @filter         = h(params[:filter]) || "*"
        @count          = h(params[:count] || 100).to_i
        @current_cursor = h(params[:cursor]).to_i
        @prev_cursor    = h(params[:prev_cursor]).to_i
        @filter         = h(params[:filter]) || "*"
        @count          = h(params[:count] || 100).to_i
        @current_cursor = h(params[:cursor]).to_i
        @prev_cursor    = h(params[:prev_cursor]).to_i
        @digest = h(params[:digest])
        digests.delete_by_digest(h(params[:digest]))
        expiring_digests.delete_by_digest(h(params[:digest]))
        @digest = h(params[:digest])