Dalli Meta Protocol request_formatter.rb self.meta_set injection
Description
Dalli up to 3.2.2 suffers an injection vulnerability in the Meta Protocol Handler that can be exploited with high complexity leading to arbitrary command injection.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
Dalli up to 3.2.2 suffers an injection vulnerability in the Meta Protocol Handler that can be exploited with high complexity leading to arbitrary command injection.
Vulnerability
Overview
CVE-2022-4064 affects Dalli, a high-performance memcached client for Ruby, in versions up to and including 3.2.2. The vulnerability exists in the self.meta_set function within the file lib/dalli/protocol/meta/request_formatter.rb, which is part of the Meta Protocol Handler. The flaw allows an injection attack by manipulating the cas or ttl arguments, enabling an attacker to inject arbitrary memcached commands [1].
Exploitation
Details
The attack is remotely exploitable but requires a high degree of complexity, making it difficult to execute. The exploit requires crafting a request where the cas or ttl parameter contains newlines and malicious memcached commands, such as \nset importantkey 1 1000 8\ninjected [2]. Because the parameters are not properly sanitized, they can be injected directly into the memcached protocol stream, potentially allowing an attacker to perform arbitrary operations on the memcached server, including writing or overwriting keys [1].
Impact
If successfully exploited, an attacker could perform unauthorized operations on the memcached server, such as modifying or deleting cached data, or potentially extracting sensitive information depending on the caching strategy. This could lead to data integrity issues or, in scenarios where cached data influences application behavior, further compromise [1]. The severity is considered problematic, with a high attack complexity that limits real-world exploitability [1].
Mitigation
The vulnerability has been patched in Dalli version 3.2.3. The fix involves properly validating and sanitizing the cas and ttl parameters so that they are excluded if they are zero or non-numeric [2]. Users are strongly recommended to upgrade to version 3.2.3 or later to mitigate the risk. No workarounds have been provided by the vendor [1].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
dalliRubyGems | < 3.2.3 | 3.2.3 |
Affected products
2- Dalli/Dallidescription
Patches
148d594dae559Fixes #932 (#933)
6 files changed · +133 −14
CHANGELOG.md+2 −0 modified@@ -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)
lib/dalli/protocol/meta.rb+1 −0 modified@@ -44,6 +44,7 @@ def gat(key, ttl, options = nil) 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)
lib/dalli/protocol/meta/request_formatter.rb+18 −5 modified@@ -31,7 +31,7 @@ def self.meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, b 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 @@ def self.meta_set(key:, value:, bitflags: nil, cas: nil, ttl: nil, mode: :set, b 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 @@ def self.meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, 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 @@ def self.version 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 @@ def self.mode_to_token(mode) 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
test/integration/test_network.rb+6 −6 modified@@ -7,17 +7,17 @@ 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 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,17 +26,17 @@ 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 end 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 @@ 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 @@ 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
test/protocol/meta/test_request_formatter.rb+104 −1 modified@@ -77,11 +77,28 @@ 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 @@ 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 @@ 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 @@ 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",
test/test_rack_session.rb+2 −2 modified@@ -54,15 +54,15 @@ 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
Vulnerability mechanics
Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
9- github.com/petergoldstein/dalli/commit/48d594dae55934476fec61789e7a7c3700e0f50dghsapatchWEB
- github.com/petergoldstein/dalli/releases/tag/v3.2.3mitrepatch
- github.com/petergoldstein/dalli/issues/932ghsaexploitissue-trackingWEB
- github.com/advisories/GHSA-3xg8-cc8f-9wv2ghsaissue-trackingADVISORY
- nvd.nist.gov/vuln/detail/CVE-2022-4064ghsaADVISORY
- github.com/petergoldstein/dalli/pull/933ghsaissue-trackingWEB
- github.com/rubysec/ruby-advisory-db/blob/master/gems/dalli/CVE-2022-4064.ymlghsaWEB
- vuldb.commitresignaturepermissions-required
- vuldb.comghsavdb-entrytechnical-descriptionWEB
News mentions
0No linked articles in our index yet.