dalli 5.0.6 → 5.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 54f211dbcca14abc18e7e9ffc7e9fe5c992c1e95afab2b6a64e46b6ec32efa8a
4
- data.tar.gz: f655e7c63cc622876ae47400afd59139eea3b8e19f6585af223fafccad94e659
3
+ metadata.gz: da626a6bfcabdac3d5c8db15d06e6c567e7a302e6b479c2db3989d40a8e32d88
4
+ data.tar.gz: 1cdffab8886b14031f193af6e9dcd3cf26f463fc1612fb77c3ef2a8106ae41bf
5
5
  SHA512:
6
- metadata.gz: de1470d516e61e2b970c0cfaa0de3afe572124081b300ce643405813c3ba232be08da32fbc3748fbc86b39e2aaea0d6e5803d34cd9722f50c3fc47b325970c67
7
- data.tar.gz: 1835dc76b3cfebe33a4a8eca38d37dcfa8af84495e0d5af523402ddd4ffa1e3a2a8c808ce4c6e2605325f8d6eded6faa4a574022680528016f091e330e987a3e
6
+ metadata.gz: 871db8e41047a834823c5c2c1caf9c4bc47265ea8b37e48f54673c7350b719e74c34d70ce27103cef4bf6acd6fba85786052ce05a9234ae6f3e98e1d30725f70
7
+ data.tar.gz: 02c5b3085ca382fd612629af79a2d06f4dc9d960f579623a8af57f2a8e4332343467e886a12e73abe3b6c52a38504896e88e2d133eace4ec6f07e0ae68af8727
data/CHANGELOG.md CHANGED
@@ -4,6 +4,33 @@ Dalli Changelog
4
4
  Unreleased
5
5
  ==========
6
6
 
7
+ 5.0.8
8
+ ==========
9
+
10
+ Bug fixes:
11
+
12
+ - Fix multi-server `get_multi` stopping at an empty value (#1170)
13
+ - The pipelined reply parser took a hit on a zero-length value (`VA 0`) for the terminating `MN`, so that server's remaining keys were silently missing from the result
14
+ - Its value terminator and any replies not yet parsed stayed on the connection and were read as the replies to later commands on it. Those commands could fail with `Dalli::DalliError: Response error`, or a single-key `get` could silently return another key's value
15
+ - An empty value is now returned as `''`, like a single-key `get` and single-server `get_multi`
16
+ - Affects the meta protocol since 3.2.0 (the default since 5.0.0)
17
+ - Thanks to Julian Richard Contreras for this contribution
18
+
19
+ Development:
20
+
21
+ - Fix offenses reported by RuboCop 1.91 and require `rubocop >= 1.91` (backport of #1162)
22
+
23
+ 5.0.7
24
+ ==========
25
+
26
+ Security:
27
+
28
+ - Fix memcached command injection through numeric arguments (GHSA-6wmv-xq9m-fmp7)
29
+ - The `default` argument of `incr`/`decr`, and `fetch_with_lock`'s `lock_ttl` and `recache_threshold`, were written into the meta protocol command without conversion, so a String containing CRLF injected additional memcached commands (e.g. `set`, `flush_all`) on the connection
30
+ - These arguments must now be Integers, or Strings of decimal digits; anything else raises `ArgumentError` before a request is sent
31
+ - As defense in depth, `RequestFormatter` now converts every numeric flag it writes (`D`, `J`, `N`, `R`, `T`) to an Integer
32
+ - Thanks to oss-security-shop for the report
33
+
7
34
  5.0.6
8
35
  ==========
9
36
 
data/Gemfile CHANGED
@@ -18,7 +18,7 @@ group :development, :test do
18
18
  gem 'rack', '~> 3'
19
19
  gem 'rack-session'
20
20
  gem 'rake', '~> 13.0'
21
- gem 'rubocop'
21
+ gem 'rubocop', '>= 1.91' # disable-next directives require 1.91+
22
22
  gem 'rubocop-minitest'
23
23
  gem 'rubocop-performance'
24
24
  gem 'rubocop-rake'
data/lib/dalli/client.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'digest/md5'
4
4
 
5
- # encoding: ascii
6
5
  module Dalli
7
6
  ##
8
7
  # Dalli::Client is the main class which developers will use to interact with
@@ -144,7 +143,7 @@ module Dalli
144
143
  # Fetch multiple keys efficiently.
145
144
  # If a block is given, yields key/value pairs one at a time.
146
145
  # Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
147
- # rubocop:disable Style/ExplicitBlockArgument
146
+ # rubocop:disable-next Style/ExplicitBlockArgument
148
147
  def get_multi(*keys)
149
148
  keys.flatten!
150
149
  keys.compact!
@@ -156,7 +155,6 @@ module Dalli
156
155
  get_multi_hash(keys)
157
156
  end
158
157
  end
159
- # rubocop:enable Style/ExplicitBlockArgument
160
158
 
161
159
  ##
162
160
  # Fetch multiple keys efficiently, including available metadata such as CAS.
@@ -225,6 +223,8 @@ module Dalli
225
223
  def fetch_with_lock(key, ttl: nil, lock_ttl: 30, recache_threshold: nil, req_options: nil, &block)
226
224
  raise ArgumentError, 'Block is required for fetch_with_lock' unless block_given?
227
225
 
226
+ validate_integer!(:lock_ttl, lock_ttl)
227
+ validate_integer!(:recache_threshold, recache_threshold)
228
228
  key = key.to_s
229
229
  key = @key_manager.validate_key(key)
230
230
 
@@ -400,6 +400,8 @@ module Dalli
400
400
  ##
401
401
  # Incr adds the given amount to the counter on the memcached server.
402
402
  # Amt must be a positive integer value.
403
+ # Default, if given, must be an Integer (or a String of decimal digits);
404
+ # anything else raises ArgumentError.
403
405
  #
404
406
  # If default is nil, the counter must already exist or the operation
405
407
  # will fail and will return nil. Otherwise this method will return
@@ -412,6 +414,7 @@ module Dalli
412
414
  # If the value already exists, it must have been set with raw: true
413
415
  def incr(key, amt = 1, ttl = nil, default = nil)
414
416
  check_positive!(amt)
417
+ validate_integer!(:default, default)
415
418
 
416
419
  perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
417
420
  end
@@ -419,6 +422,8 @@ module Dalli
419
422
  ##
420
423
  # Decr subtracts the given amount from the counter on the memcached server.
421
424
  # Amt must be a positive integer value.
425
+ # Default, if given, must be an Integer (or a String of decimal digits);
426
+ # anything else raises ArgumentError.
422
427
  #
423
428
  # memcached counters are unsigned and cannot hold negative values. Calling
424
429
  # decr on a counter which is 0 will just return 0.
@@ -434,6 +439,7 @@ module Dalli
434
439
  # If the value already exists, it must have been set with raw: true
435
440
  def decr(key, amt = 1, ttl = nil, default = nil)
436
441
  check_positive!(amt)
442
+ validate_integer!(:default, default)
437
443
 
438
444
  perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
439
445
  end
@@ -617,6 +623,19 @@ module Dalli
617
623
  raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
618
624
  end
619
625
 
626
+ # Numeric arguments that become meta protocol flags (GHSA-6wmv-xq9m-fmp7).
627
+ # RequestFormatter converts them to Integer as the wire-level backstop;
628
+ # checking here too raises a clean ArgumentError before the request
629
+ # starts, instead of Protocol::Base#request logging it as unexpected and
630
+ # closing the connection.
631
+ def validate_integer!(name, value)
632
+ return if value.nil?
633
+
634
+ value.is_a?(String) ? Integer(value, 10) : Integer(value)
635
+ rescue ArgumentError, TypeError, FloatDomainError
636
+ raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
637
+ end
638
+
620
639
  def cas_core(key, always_set, ttl = nil, req_options = nil)
621
640
  (value, cas) = perform(:cas, key)
622
641
  return if value.nil? && !always_set
@@ -153,7 +153,7 @@ module Dalli
153
153
  response_processor.meta_set_append_prepend unless quiet?
154
154
  end
155
155
 
156
- # rubocop:disable Metrics/ParameterLists
156
+ # rubocop:disable-next Metrics/ParameterLists
157
157
  def write_append_prepend_req(mode, key, value, ttl = nil, cas = nil, _options = {})
158
158
  ttl = TtlSanitizer.sanitize(ttl) if ttl
159
159
  req = RequestFormatter.meta_set(key: key, value: value,
@@ -161,7 +161,6 @@ module Dalli
161
161
  write("#{req}#{value}#{TERMINATOR}")
162
162
  @connection_manager.flush unless quiet?
163
163
  end
164
- # rubocop:enable Metrics/ParameterLists
165
164
 
166
165
  # Delete Commands
167
166
  def delete(key, cas)
@@ -43,10 +43,11 @@ module Dalli
43
43
  # This saves 2 bytes per request and skips parsing on response.
44
44
  cmd << (skip_flags ? ' v' : ' v f') if value
45
45
  cmd << ' c' if return_cas
46
- cmd << " T#{ttl}" if ttl
46
+ cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
47
47
  cmd << ' k q s' if quiet # Return the key in the response if quiet
48
- cmd << " N#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
49
- cmd << " R#{recache_ttl}" if recache_ttl # Thundering herd: win recache if TTL below threshold
48
+ cmd << " N#{integer_flag(:vivify_ttl, vivify_ttl)}" if vivify_ttl # Thundering herd: vivify on miss
49
+ # Thundering herd: win recache if TTL below threshold
50
+ cmd << " R#{integer_flag(:recache_ttl, recache_ttl)}" if recache_ttl
50
51
  cmd << ' h' if return_hit_status # Return hit status (0 or 1)
51
52
  cmd << ' l' if return_last_access # Return seconds since last access
52
53
  cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
@@ -74,13 +75,14 @@ module Dalli
74
75
  cmd << ' b' if base64
75
76
  cmd << " F#{bitflags}" if bitflags
76
77
  cmd << cas_string(cas)
77
- cmd << " T#{ttl}" if ttl
78
+ cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
78
79
  cmd << " M#{mode_to_token(mode)}"
79
80
  cmd << ' q' if quiet
80
81
  cmd << TERMINATOR
81
82
  end
82
83
 
83
84
  def multi_meta_set(entries, ttl: nil)
85
+ ttl = integer_flag(:ttl, ttl) if ttl
84
86
  buffer = ''.b
85
87
  entries.each do |key, pair|
86
88
  value, bitflags = pair
@@ -104,7 +106,7 @@ module Dalli
104
106
  def meta_delete(key:, cas: nil, ttl: nil, quiet: false, stale: false)
105
107
  cmd = "md #{encoded_key(key)}"
106
108
  cmd << cas_string(cas)
107
- cmd << " T#{ttl}" if ttl
109
+ cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
108
110
  cmd << ' I' if stale # Mark stale instead of deleting
109
111
  cmd << ' q' if quiet
110
112
  cmd << TERMINATOR
@@ -120,10 +122,10 @@ module Dalli
120
122
 
121
123
  def meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, quiet: false)
122
124
  cmd = "ma #{encoded_key(key)} v"
123
- cmd << " D#{delta}" if delta
124
- cmd << " J#{initial}" if initial
125
+ cmd << " D#{integer_flag(:delta, delta)}" if delta
126
+ cmd << " J#{integer_flag(:initial, initial)}" if initial
125
127
  # Always set a TTL if an initial value is specified
126
- cmd << " N#{ttl || 0}" if ttl || initial
128
+ cmd << " N#{ttl ? integer_flag(:ttl, ttl) : 0}" if ttl || initial
127
129
  cmd << cas_string(cas)
128
130
  cmd << ' q' if quiet
129
131
  cmd << " M#{incr ? 'I' : 'D'}"
@@ -169,6 +171,18 @@ module Dalli
169
171
 
170
172
  private
171
173
 
174
+ # Numeric flag values are written straight into the command line, so a
175
+ # value that isn't an integer -- e.g. a String carrying CRLF -- would let
176
+ # the caller inject further memcached commands (GHSA-6wmv-xq9m-fmp7).
177
+ # Converting to Integer means only digits ever reach the wire. Strings
178
+ # are parsed as base 10 so "010" means 10, as memcached would read it,
179
+ # rather than octal 8.
180
+ def integer_flag(name, value)
181
+ value.is_a?(String) ? Integer(value, 10) : Integer(value)
182
+ rescue ArgumentError, TypeError, FloatDomainError
183
+ raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
184
+ end
185
+
172
186
  def mode_to_token(mode)
173
187
  case mode
174
188
  when :add
@@ -204,8 +204,10 @@ module Dalli
204
204
  # We have a complete response that has no body.
205
205
  # This is either the response to the terminating
206
206
  # noop or, if the status is not MN, an intermediate
207
- # error response that needs to be discarded.
208
- return [true, header_len] if body_len.zero?
207
+ # error response that needs to be discarded. A hit
208
+ # on an empty value (VA 0) still has a body -- just
209
+ # its terminator -- so it's parsed below as a value.
210
+ return [true, header_len] if body_len.zero? && tokens.first != VA
209
211
 
210
212
  resp_size = header_len + body_len + TERMINATOR.length
211
213
  # The header is in the buffer, but the body is not. As we don't have
data/lib/dalli/socket.rb CHANGED
@@ -124,7 +124,7 @@ module Dalli
124
124
  # Returns true for an unmodified TCPSocket on Ruby 3.0+, or for resolv-replace >= 0.2.0
125
125
  # which forwards keyword arguments through its patch.
126
126
  # Returns false when monkey-patched by gems like socksify or resolv-replace < 0.2.0.
127
- # rubocop:disable ThreadSafety/ClassInstanceVariable
127
+ # rubocop:disable-next ThreadSafety/ClassInstanceVariable
128
128
  def self.supports_connect_timeout?
129
129
  return @supports_connect_timeout if defined?(@supports_connect_timeout)
130
130
 
@@ -135,7 +135,6 @@ module Dalli
135
135
  end
136
136
  end
137
137
  end
138
- # rubocop:enable ThreadSafety/ClassInstanceVariable
139
138
 
140
139
  def self.create_socket_with_timeout(host, port, options)
141
140
  if supports_connect_timeout?
@@ -192,14 +191,13 @@ module Dalli
192
191
 
193
192
  # Detect and cache the correct pack format for struct timeval on this platform.
194
193
  # Different architectures have different sizes for time_t and suseconds_t.
195
- # rubocop:disable ThreadSafety/ClassInstanceVariable
194
+ # rubocop:disable-next ThreadSafety/ClassInstanceVariable
196
195
  def self.timeval_pack_format(sock)
197
196
  @timeval_pack_format ||= begin
198
197
  expected_size = sock.getsockopt(::Socket::SOL_SOCKET, ::Socket::SO_RCVTIMEO).data.bytesize
199
198
  TIMEVAL_PACK_FORMATS.find { |fmt| TIMEVAL_TEST_VALUES.pack(fmt).bytesize == expected_size } || 'll'
200
199
  end
201
200
  end
202
- # rubocop:enable ThreadSafety/ClassInstanceVariable
203
201
 
204
202
  def self.pack_timeval(sock, seconds, microseconds)
205
203
  [seconds, microseconds].pack(timeval_pack_format(sock))
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '5.0.6'
4
+ VERSION = '5.0.8'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.6'
7
7
  end
@@ -16,11 +16,10 @@ module Rack
16
16
  attr_reader :data
17
17
 
18
18
  # Don't freeze this until we fix the specs/implementation
19
- # rubocop:disable Style/MutableConstant
19
+ # rubocop:disable-next Style/MutableConstant
20
20
  DEFAULT_DALLI_OPTIONS = {
21
21
  namespace: 'rack:session'
22
22
  }
23
- # rubocop:enable Style/MutableConstant
24
23
 
25
24
  # Brings in a new Rack::Session::Dalli middleware with the given
26
25
  # `:memcache_server`. The server is either a hostname, or a
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dalli
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.0.6
4
+ version: 5.0.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
@@ -87,7 +87,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
87
87
  - !ruby/object:Gem::Version
88
88
  version: '0'
89
89
  requirements: []
90
- rubygems_version: 4.0.16
90
+ rubygems_version: 4.0.21
91
91
  specification_version: 4
92
92
  summary: High performance memcached client for Ruby
93
93
  test_files: []