CVE-2022-4064 (published 2022-11-19T19:15Z)

A vulnerability was found in Dalli. It has been classified as problematic. Affected is the function self.meta_set of the file lib/dalli/protocol/meta/request_formatter.rb of the component Meta Protocol Handler. The manipulation leads to injection. The exploit has been disclosed to the public and may be used. The name of the patch is 48d594dae55934476fec61789e7a7c3700e0f50d. It is recommended to apply a patch to fix this issue. VDB-214026 is the identifier assigned to this vulnerability.

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

Weakness information:
cwe_id: CWE-74
cwe_name: Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')
description: The product constructs all or part of a command, data structure, or record using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify how it is parsed or interpreted when it is sent to a downstream component.
extended_description: Software or other automated logic has certain assumptions about what constitutes data and control respectively. It is the lack of verification of these assumptions for user-controlled input that leads to injection problems. Injection problems encompass a wide variety of issues -- all mitigated in very different ways and usually attempted in order to alter the control flow of the process. For this reason, the most effective way to discuss these weaknesses is to note the distinct features that classify them as injection weaknesses. The most important issue to note is that all injection problems share one thing in common -- i.e., they allow for the injection of control plane data into the user-controlled data plane. This means that the execution of the process may be altered by sending code in through legitimate data channels, using no other mechanism. While buffer overflows, and many other flaws, involve the use of some further issue to gain execution, injection problems need only for the data to be parsed.
         

Fix information:
repo_url: https://github.com/petergoldstein/dalli
commit_details: repo_url: https://github.com/petergoldstein/dalli
author: Peter Goldstein
author_date: 2022-10-27 17:58:52-04:00
msg: Fixes #932 (#933)

Do some input sanitization for the meta protocol. Since this protocol uses raw test, it is possible to execute an injection attack against unsanitized inputs.

This commit explicitly sanitizes the CAS arguments and the ttl passed to flush.

I also added some missing tests for meta_arithmetic and fixed a few lints.
repository: repo_url: https://github.com/petergoldstein/dalli
repo_name: petergoldstein/dalli
description: High performance memcached client for Ruby
repo_language: Ruby
owner: petergoldstein
file_changes: filename: CHANGELOG.md
change_type: ModificationType.MODIFY
diff: @@ -4,6 +4,8 @@ Dalli Changelog
 Unreleased
 ==========
 
+- Sanitize CAS inputs to ensure additional commands are not passed to memcached (xhzeem / petergoldstein)
+- Sanitize input to flush command to ensure additional commands are not passed to memcached (xhzeem / petergoldstein)
 - Namespaces passed as procs are now evaluated every time, as opposed to just on initialization (nrw505)
 - Fix missing require of uri in ServerConfigParser (adam12)
 - Fix link to the CHANGELOG.md file in README.md (rud)

programming_language: Markdown
filename: meta.rb
change_type: ModificationType.MODIFY
diff: @@ -44,6 +44,7 @@ module Dalli
       end
 
       def touch(key, ttl)
+        ttl = TtlSanitizer.sanitize(ttl)
         encoded_key, base64 = KeyRegularizer.encode(key)
         req = RequestFormatter.meta_get(key: encoded_key, ttl: ttl, value: false, base64: base64)
         write(req)

programming_language: Ruby
filename: request_formatter.rb
change_type: ModificationType.MODIFY
diff: @@ -31,7 +31,7 @@ module Dalli
           cmd << ' c' unless %i[append prepend].include?(mode)
           cmd << ' b' if base64
           cmd << " F#{bitflags}" if bitflags
-          cmd << " C#{cas}" if cas && !cas.zero?
+          cmd << cas_string(cas)
           cmd << " T#{ttl}" if ttl
           cmd << " M#{mode_to_token(mode)}"
           cmd << ' q' if quiet
@@ -43,7 +43,7 @@ module Dalli
         def self.meta_delete(key:, cas: nil, ttl: nil, base64: false, quiet: false)
           cmd = "md #{key}"
           cmd << ' b' if base64
-          cmd << " C#{cas}" if cas && !cas.zero?
+          cmd << cas_string(cas)
           cmd << " T#{ttl}" if ttl
           cmd << ' q' if quiet
           cmd + TERMINATOR
@@ -54,8 +54,9 @@ module Dalli
           cmd << ' b' if base64
           cmd << " D#{delta}" if delta
           cmd << " J#{initial}" if initial
-          cmd << " C#{cas}" if cas && !cas.zero?
-          cmd << " N#{ttl}" if ttl
+          # Always set a TTL if an initial value is specified
+          cmd << " N#{ttl || 0}" if ttl || initial
+          cmd << cas_string(cas)
           cmd << ' q' if quiet
           cmd << " M#{incr ? 'I' : 'D'}"
           cmd + TERMINATOR
@@ -75,7 +76,7 @@ module Dalli
 
         def self.flush(delay: nil, quiet: false)
           cmd = +'flush_all'
-          cmd << " #{delay}" if delay
+          cmd << " #{parse_to_64_bit_int(delay, 0)}" if delay
           cmd << ' noreply' if quiet
           cmd + TERMINATOR
         end
@@ -102,6 +103,18 @@ module Dalli
           end
         end
         # rubocop:enable Metrics/MethodLength
+
+        def self.cas_string(cas)
+          cas = parse_to_64_bit_int(cas, nil)
+          cas.nil? || cas.zero? ? '' : " C#{cas}"
+        end
+
+        def self.parse_to_64_bit_int(val, default)
+          val.nil? ? nil : Integer(val)
+        rescue ArgumentError
+          # Sanitize to default if it isn't parsable as an integer
+          default
+        end
       end
     end
   end

programming_language: Ruby
filename: test_network.rb
change_type: ModificationType.MODIFY
diff: @@ -7,8 +7,8 @@ describe 'Network' do
     describe "using the #{p} protocol" do
       describe 'assuming a bad network' do
         it 'handle no server available' do
+          dc = Dalli::Client.new 'localhost:19333'
           assert_raises Dalli::RingError, message: 'No server available' do
-            dc = Dalli::Client.new 'localhost:19333'
             dc.get 'foo'
           end
         end
@@ -16,8 +16,8 @@ describe 'Network' do
         describe 'with a fake server' do
           it 'handle connection reset' do
             memcached_mock(->(sock) { sock.close }) do
+              dc = Dalli::Client.new('localhost:19123')
               assert_raises Dalli::RingError, message: 'No server available' do
-                dc = Dalli::Client.new('localhost:19123')
                 dc.get('abc')
               end
             end
@@ -26,8 +26,8 @@ describe 'Network' do
           it 'handle connection reset with unix socket' do
             socket_path = MemcachedMock::UNIX_SOCKET_PATH
             memcached_mock(->(sock) { sock.close }, :start_unix, socket_path) do
+              dc = Dalli::Client.new(socket_path)
               assert_raises Dalli::RingError, message: 'No server available' do
-                dc = Dalli::Client.new(socket_path)
                 dc.get('abc')
               end
             end
@@ -35,8 +35,8 @@ describe 'Network' do
 
           it 'handle malformed response' do
             memcached_mock(->(sock) { sock.write('123') }) do
+              dc = Dalli::Client.new('localhost:19123')
               assert_raises Dalli::RingError, message: 'No server available' do
-                dc = Dalli::Client.new('localhost:19123')
                 dc.get('abc')
               end
             end
@@ -47,8 +47,8 @@ describe 'Network' do
                              sleep(0.6)
                              sock.close
                            }, :delayed_start) do
+              dc = Dalli::Client.new('localhost:19123')
               assert_raises Dalli::RingError, message: 'No server available' do
-                dc = Dalli::Client.new('localhost:19123')
                 dc.get('abc')
               end
             end
@@ -59,8 +59,8 @@ describe 'Network' do
                              sleep(0.6)
                              sock.write('giraffe')
                            }) do
+              dc = Dalli::Client.new('localhost:19123')
               assert_raises Dalli::RingError, message: 'No server available' do
-                dc = Dalli::Client.new('localhost:19123')
                 dc.get('abc')
               end
             end

programming_language: Ruby
filename: test_request_formatter.rb
change_type: ModificationType.MODIFY
diff: @@ -77,11 +77,28 @@ describe Dalli::Protocol::Meta::RequestFormatter do
                    Dalli::Protocol::Meta::RequestFormatter.meta_set(key: key, value: val, bitflags: bitflags, cas: cas)
     end
 
+    it 'excludes CAS if set to 0' do
+      assert_equal "ms #{key} #{val.bytesize} c F#{bitflags} MS\r\n#{val}\r\n",
+                   Dalli::Protocol::Meta::RequestFormatter.meta_set(key: key, value: val, bitflags: bitflags, cas: 0)
+    end
+
+    it 'excludes non-numeric CAS values' do
+      assert_equal "ms #{key} #{val.bytesize} c F#{bitflags} MS\r\n#{val}\r\n",
+                   Dalli::Protocol::Meta::RequestFormatter.meta_set(key: key, value: val, bitflags: bitflags,
+                                                                    cas: "\nset importantkey 1 1000 8\ninjected")
+    end
+
     it 'sets the quiet mode if configured' do
       assert_equal "ms #{key} #{val.bytesize} c F#{bitflags} MS q\r\n#{val}\r\n",
                    Dalli::Protocol::Meta::RequestFormatter.meta_set(key: key, value: val, bitflags: bitflags,
                                                                     quiet: true)
     end
+
+    it 'sets the base64 mode if configured' do
+      assert_equal "ms #{key} #{val.bytesize} c b F#{bitflags} MS\r\n#{val}\r\n",
+                   Dalli::Protocol::Meta::RequestFormatter.meta_set(key: key, value: val, bitflags: bitflags,
+                                                                    base64: true)
+    end
   end
 
   describe 'meta_delete' do
@@ -93,7 +110,7 @@ describe Dalli::Protocol::Meta::RequestFormatter do
                    Dalli::Protocol::Meta::RequestFormatter.meta_delete(key: key)
     end
 
-    it 'returns incorporates CAS when passed cas' do
+    it 'incorporates CAS when passed cas' do
       assert_equal "md #{key} C#{cas}\r\n",
                    Dalli::Protocol::Meta::RequestFormatter.meta_delete(key: key, cas: cas)
     end
@@ -102,6 +119,87 @@ describe Dalli::Protocol::Meta::RequestFormatter do
       assert_equal "md #{key} q\r\n",
                    Dalli::Protocol::Meta::RequestFormatter.meta_delete(key: key, quiet: true)
     end
+
+    it 'excludes CAS when set to 0' do
+      assert_equal "md #{key}\r\n",
+                   Dalli::Protocol::Meta::RequestFormatter.meta_delete(key: key, cas: 0)
+    end
+
+    it 'excludes non-numeric CAS values' do
+      assert_equal "md #{key}\r\n",
+                   Dalli::Protocol::Meta::RequestFormatter.meta_delete(key: key,
+                                                                       cas: "\nset importantkey 1 1000 8\ninjected")
+    end
+
+    it 'sets the base64 mode if configured' do
+      assert_equal "md #{key} b\r\n",
+                   Dalli::Protocol::Meta::RequestFormatter.meta_delete(key: key, base64: true)
+    end
+  end
+
+  describe 'meta_arithmetic' do
+    let(:key) { SecureRandom.hex(4) }
+    let(:delta) { rand(500..999) }
+    let(:initial) { rand(500..999) }
+    let(:cas) { rand(500..999) }
+    let(:ttl) { rand(500..999) }
+
+    it 'returns the expected string with the default N flag when passed non-nil key, delta, and initial' do
+      assert_equal "ma #{key} v D#{delta} J#{initial} N0 MI\r\n",
+                   Dalli::Protocol::Meta::RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial)
+    end
+
+    it 'excludes the J and N flags when initial is nil and ttl is not set' do
+      assert_equal "ma #{key} v D#{delta} MI\r\n",
+                   Dalli::Protocol::Meta::RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: nil)
+    end
+
+    it 'omits the D flag is delta is nil' do
+      assert_equal "ma #{key} v J#{initial} N0 MI\r\n",
+                   Dalli::Protocol::Meta::RequestFormatter.meta_arithmetic(key: key, delta: nil, initial: initial)
+    end
+
+    it 'uses ttl for the N flag when ttl passed explicitly along with an initial value' do
+      assert_equal "ma #{key} v D#{delta} J#{initial} N#{ttl} MI\r\n",
+                   Dalli::Protocol::Meta::RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial,
+                                                                           ttl: ttl)
+    end
+
+    it 'incorporates CAS when passed cas' do
+      assert_equal "ma #{key} v D#{delta} J#{initial} N0 C#{cas} MI\r\n",
+                   Dalli::Protocol::Meta::RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial,
+                                                                           cas: cas)
+    end
+
+    it 'excludes CAS when CAS is set to 0' do
+      assert_equal "ma #{key} v D#{delta} J#{initial} N0 MI\r\n",
+                   Dalli::Protocol::Meta::RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial,
+                                                                           cas: 0)
+    end
+
+    it 'includes the N flag when ttl passed explicitly with a nil initial value' do
+      assert_equal "ma #{key} v D#{delta} N#{ttl} MI\r\n",
+                   Dalli::Protocol::Meta::RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: nil,
+                                                                           ttl: ttl)
+    end
+
+    it 'swaps from MI to MD when the incr value is explicitly false' do
+      assert_equal "ma #{key} v D#{delta} J#{initial} N0 MD\r\n",
+                   Dalli::Protocol::Meta::RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial,
+                                                                           incr: false)
+    end
+
+    it 'includes the quiet flag when specified' do
+      assert_equal "ma #{key} v D#{delta} J#{initial} N0 q MI\r\n",
+                   Dalli::Protocol::Meta::RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial,
+                                                                           quiet: true)
+    end
+
+    it 'sets the base64 mode if configured' do
+      assert_equal "ma #{key} v b D#{delta} J#{initial} N0 MI\r\n",
+                   Dalli::Protocol::Meta::RequestFormatter.meta_arithmetic(key: key, delta: delta, initial: initial,
+                                                                           base64: true)
+    end
   end
 
   describe 'meta_noop' do
@@ -130,6 +228,11 @@ describe Dalli::Protocol::Meta::RequestFormatter do
       assert_equal "flush_all #{delay}\r\n", Dalli::Protocol::Meta::RequestFormatter.flush(delay: delay)
     end
 
+    it 'santizes the delay argument' do
+      delay = "\nset importantkey 1 1000 8\ninjected"
+      assert_equal "flush_all 0\r\n", Dalli::Protocol::Meta::RequestFormatter.flush(delay: delay)
+    end
+
     it 'adds noreply with a delay and quiet argument' do
       delay = rand(1000..1999)
       assert_equal "flush_all #{delay} noreply\r\n",

programming_language: Ruby
filename: test_rack_session.rb
change_type: ModificationType.MODIFY
diff: @@ -54,15 +54,15 @@ describe Rack::Session::Dalli do
   let(:incrementor) { Rack::Lint.new(incrementor_proc) }
 
   it 'faults on no connection' do
+    rsd = Rack::Session::Dalli.new(incrementor, memcache_server: 'nosuchserver')
     assert_raises Dalli::RingError do
-      rsd = Rack::Session::Dalli.new(incrementor, memcache_server: 'nosuchserver')
       rsd.data.with { |c| c.set('ping', '') }
     end
   end
 
   it 'connects to existing server' do
+    rsd = Rack::Session::Dalli.new(incrementor, namespace: 'test:rack:session')
     assert_silent do
-      rsd = Rack::Session::Dalli.new(incrementor, namespace: 'test:rack:session')
       rsd.data.with { |c| c.set('ping', '') }
     end
   end

programming_language: Ruby