dalli 4.3.3 → 4.3.5

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: 57b78e30ee409a2d742fc47ee57ccdd482fe9c75f369366ae315b7ef8b649934
4
- data.tar.gz: b8cad66f3cba53bbcb84b18f406eed60f22209c10dd4a312063a1e13189185ca
3
+ metadata.gz: 96ac26bd7ef748d2407efaa5d1bcfe8f762c2eb47d67f85fb6e19f789926abae
4
+ data.tar.gz: 617744189a342d6b39bd741a685b997bb0fbbae82afb3aeaafbf87ac79f6fe29
5
5
  SHA512:
6
- metadata.gz: 18b26e3c4aa30e5f8b4195d95307afca6c9240dd1154a36966d40d52047e638758850ec06e2a40fd1bd8e69fe150dee7409b1f4a565f0a80e397aaf115315463
7
- data.tar.gz: b6db69ecbc1e6d34587d8ec29b861f6a71c863b36229d3c642e3d0e646ba6234e3bac04225d64ff174d12f35b728663e82e1d2b5f7e93dc9ac3e1ff33fd58db3
6
+ metadata.gz: 7c0f37ce570d445484045722da9d5463879d825cb0ff2c8436e59852e0a996b44be61ebf942eb7bfc14846fb64991edf4cbf75c5bfa5871d8f3409b8a3fae25d
7
+ data.tar.gz: 3a96488a8bb4da71afbb724745da958bec01ce5e9e229d7b5f0be0f5bd56513a0de7acefba6feab012efab5cd8d9971a5956a4aa5f1c9b154f503721bd703b72
data/CHANGELOG.md CHANGED
@@ -1,6 +1,36 @@
1
1
  Dalli Changelog
2
2
  =====================
3
3
 
4
+ Unreleased
5
+ ==========
6
+
7
+ 4.3.5
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 only clients using `protocol: :meta`; the default binary protocol is not affected
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
+ 4.3.4
24
+ ==========
25
+
26
+ Security:
27
+
28
+ - Fix memcached command injection through numeric arguments with the meta protocol (GHSA-6wmv-xq9m-fmp7)
29
+ - With `protocol: :meta`, the `default` argument of `incr`/`decr`, and `fetch_with_lock`'s `lock_ttl` and `recache_threshold`, were written into the command without conversion, so a String containing CRLF injected additional memcached commands (e.g. `set`, `flush_all`) on the connection. The default binary protocol is not affected
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, the meta `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
+
4
34
  4.3.3
5
35
  ==========
6
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
@@ -3,7 +3,6 @@
3
3
  require 'digest/md5'
4
4
  require 'set'
5
5
 
6
- # encoding: ascii
7
6
  module Dalli
8
7
  ##
9
8
  # Dalli::Client is the main class which developers will use to interact with
@@ -152,7 +151,7 @@ module Dalli
152
151
  # Fetch multiple keys efficiently.
153
152
  # If a block is given, yields key/value pairs one at a time.
154
153
  # Otherwise returns a hash of { 'key' => 'value', 'key2' => 'value1' }
155
- # rubocop:disable Style/ExplicitBlockArgument
154
+ # rubocop:disable-next Style/ExplicitBlockArgument
156
155
  def get_multi(*keys)
157
156
  keys.flatten!
158
157
  keys.compact!
@@ -164,7 +163,6 @@ module Dalli
164
163
  get_multi_hash(keys)
165
164
  end
166
165
  end
167
- # rubocop:enable Style/ExplicitBlockArgument
168
166
 
169
167
  ##
170
168
  # Fetch multiple keys efficiently, including available metadata such as CAS.
@@ -237,6 +235,8 @@ module Dalli
237
235
  raise ArgumentError, 'Block is required for fetch_with_lock' unless block_given?
238
236
 
239
237
  raise_unless_meta_protocol!
238
+ validate_integer!(:lock_ttl, lock_ttl)
239
+ validate_integer!(:recache_threshold, recache_threshold)
240
240
 
241
241
  key = key.to_s
242
242
  key = @key_manager.validate_key(key)
@@ -402,6 +402,8 @@ module Dalli
402
402
  ##
403
403
  # Incr adds the given amount to the counter on the memcached server.
404
404
  # Amt must be a positive integer value.
405
+ # Default, if given, must be an Integer (or a String of decimal digits);
406
+ # anything else raises ArgumentError.
405
407
  #
406
408
  # If default is nil, the counter must already exist or the operation
407
409
  # will fail and will return nil. Otherwise this method will return
@@ -414,6 +416,7 @@ module Dalli
414
416
  # If the value already exists, it must have been set with raw: true
415
417
  def incr(key, amt = 1, ttl = nil, default = nil)
416
418
  check_positive!(amt)
419
+ validate_integer!(:default, default)
417
420
 
418
421
  perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
419
422
  end
@@ -421,6 +424,8 @@ module Dalli
421
424
  ##
422
425
  # Decr subtracts the given amount from the counter on the memcached server.
423
426
  # Amt must be a positive integer value.
427
+ # Default, if given, must be an Integer (or a String of decimal digits);
428
+ # anything else raises ArgumentError.
424
429
  #
425
430
  # memcached counters are unsigned and cannot hold negative values. Calling
426
431
  # decr on a counter which is 0 will just return 0.
@@ -436,6 +441,7 @@ module Dalli
436
441
  # If the value already exists, it must have been set with raw: true
437
442
  def decr(key, amt = 1, ttl = nil, default = nil)
438
443
  check_positive!(amt)
444
+ validate_integer!(:default, default)
439
445
 
440
446
  perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
441
447
  end
@@ -577,6 +583,14 @@ module Dalli
577
583
  raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
578
584
  end
579
585
 
586
+ # Numeric arguments that become meta protocol flags (GHSA-6wmv-xq9m-fmp7).
587
+ # Checked before the request starts so a bad value raises a clean
588
+ # ArgumentError, instead of Protocol::Base#request logging it as
589
+ # unexpected and closing the connection.
590
+ def validate_integer!(name, value)
591
+ Dalli::Protocol::Meta::RequestFormatter.integer_flag(name, value) unless value.nil?
592
+ end
593
+
580
594
  def cas_core(key, always_set, ttl = nil, req_options = nil)
581
595
  (value, cas) = perform(:cas, key)
582
596
  return if value.nil? && !always_set
@@ -94,7 +94,7 @@ module Dalli
94
94
  # When a block is given, yields (key, value, cas) for each response,
95
95
  # avoiding intermediate Hash allocation. Returns nil.
96
96
  # Without a block, returns a Hash of { key => [value, cas] }.
97
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
97
+ # rubocop:disable-next Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
98
98
  def pipeline_next_responses(&block)
99
99
  reconnect_on_pipeline_complete!
100
100
  values = nil
@@ -128,7 +128,6 @@ module Dalli
128
128
  rescue SystemCallError, *TIMEOUT_ERRORS, *SSL_ERRORS, EOFError => e
129
129
  @connection_manager.error_on_request!(e)
130
130
  end
131
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
132
131
 
133
132
  # Abort current pipelined get. Generally used to signal an external
134
133
  # timeout during pipelined get. The underlying socket is
@@ -88,7 +88,7 @@ module Dalli
88
88
  }.freeze
89
89
  FORMAT = BODY_FORMATS.transform_values { |v| REQ_HEADER_FORMAT + v }
90
90
 
91
- # rubocop:disable Metrics/ParameterLists
91
+ # rubocop:disable-next Metrics/ParameterLists
92
92
  def self.standard_request(opkey:, key: nil, value: nil, opaque: 0, cas: 0, bitflags: nil, ttl: nil)
93
93
  extra_len = (bitflags.nil? ? 0 : 4) + (ttl.nil? ? 0 : 4)
94
94
  key_len = key.nil? ? 0 : key.bytesize
@@ -97,7 +97,6 @@ module Dalli
97
97
  body = [bitflags, ttl, key, value].compact
98
98
  (header + body).pack(FORMAT[opkey])
99
99
  end
100
- # rubocop:enable Metrics/ParameterLists
101
100
 
102
101
  def self.decr_incr_request(opkey:, key: nil, count: nil, initial: nil, expiry: nil)
103
102
  extra_len = 20
@@ -76,7 +76,7 @@ module Dalli
76
76
  storage_req(opkey, key, value, ttl, cas, options)
77
77
  end
78
78
 
79
- # rubocop:disable Metrics/ParameterLists
79
+ # rubocop:disable-next Metrics/ParameterLists
80
80
  def storage_req(opkey, key, value, ttl, cas, options)
81
81
  (value, bitflags) = @value_marshaller.store(key, value, options)
82
82
  ttl = TtlSanitizer.sanitize(ttl)
@@ -88,7 +88,6 @@ module Dalli
88
88
  @connection_manager.flush unless quiet?
89
89
  response_processor.storage_response unless quiet?
90
90
  end
91
- # rubocop:enable Metrics/ParameterLists
92
91
 
93
92
  def append(key, value)
94
93
  opkey = quiet? ? :appendq : :append
@@ -45,10 +45,11 @@ module Dalli
45
45
  cmd << (skip_flags ? ' v' : ' v f') if value
46
46
  cmd << ' c' if return_cas
47
47
  cmd << ' b' if base64
48
- cmd << " T#{ttl}" if ttl
48
+ cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
49
49
  cmd << ' k q s' if quiet # Return the key in the response if quiet
50
- cmd << " N#{vivify_ttl}" if vivify_ttl # Thundering herd: vivify on miss
51
- cmd << " R#{recache_ttl}" if recache_ttl # Thundering herd: win recache if TTL below threshold
50
+ cmd << " N#{integer_flag(:vivify_ttl, vivify_ttl)}" if vivify_ttl # Thundering herd: vivify on miss
51
+ # Thundering herd: win recache if TTL below threshold
52
+ cmd << " R#{integer_flag(:recache_ttl, recache_ttl)}" if recache_ttl
52
53
  cmd << ' h' if return_hit_status # Return hit status (0 or 1)
53
54
  cmd << ' l' if return_last_access # Return seconds since last access
54
55
  cmd << ' u' if skip_lru_bump # Don't bump LRU or update access stats
@@ -61,7 +62,7 @@ module Dalli
61
62
  cmd << ' b' if base64
62
63
  cmd << " F#{bitflags}" if bitflags
63
64
  cmd << cas_string(cas)
64
- cmd << " T#{ttl}" if ttl
65
+ cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
65
66
  cmd << " M#{mode_to_token(mode)}"
66
67
  cmd << ' q' if quiet
67
68
  cmd << TERMINATOR
@@ -74,7 +75,7 @@ module Dalli
74
75
  cmd = "md #{key}"
75
76
  cmd << ' b' if base64
76
77
  cmd << cas_string(cas)
77
- cmd << " T#{ttl}" if ttl
78
+ cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
78
79
  cmd << ' I' if stale # Mark stale instead of deleting
79
80
  cmd << ' q' if quiet
80
81
  cmd + TERMINATOR
@@ -83,10 +84,10 @@ module Dalli
83
84
  def self.meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, base64: false, quiet: false)
84
85
  cmd = "ma #{key} v"
85
86
  cmd << ' b' if base64
86
- cmd << " D#{delta}" if delta
87
- cmd << " J#{initial}" if initial
87
+ cmd << " D#{integer_flag(:delta, delta)}" if delta
88
+ cmd << " J#{integer_flag(:initial, initial)}" if initial
88
89
  # Always set a TTL if an initial value is specified
89
- cmd << " N#{ttl || 0}" if ttl || initial
90
+ cmd << " N#{ttl ? integer_flag(:ttl, ttl) : 0}" if ttl || initial
90
91
  cmd << cas_string(cas)
91
92
  cmd << ' q' if quiet
92
93
  cmd << " M#{incr ? 'I' : 'D'}"
@@ -136,6 +137,18 @@ module Dalli
136
137
  end
137
138
  end
138
139
 
140
+ # Numeric flag values are written straight into the command line, so a
141
+ # value that isn't an integer -- e.g. a String carrying CRLF -- would let
142
+ # the caller inject further memcached commands (GHSA-6wmv-xq9m-fmp7).
143
+ # Converting to Integer means only digits ever reach the wire. Strings
144
+ # are parsed as base 10 so "010" means 10, as memcached would read it,
145
+ # rather than octal 8.
146
+ def self.integer_flag(name, value)
147
+ value.is_a?(String) ? Integer(value, 10) : Integer(value)
148
+ rescue ArgumentError, TypeError, FloatDomainError
149
+ raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
150
+ end
151
+
139
152
  def self.cas_string(cas)
140
153
  cas = parse_to_64_bit_int(cas, nil)
141
154
  cas.nil? || cas.zero? ? '' : " C#{cas}"
@@ -175,8 +175,10 @@ module Dalli
175
175
  # We have a complete response that has no body.
176
176
  # This is either the response to the terminating
177
177
  # noop or, if the status is not MN, an intermediate
178
- # error response that needs to be discarded.
179
- return [header_len, true, nil, nil, nil] if body_len.zero?
178
+ # error response that needs to be discarded. A hit
179
+ # on an empty value (VA 0) still has a body -- just
180
+ # its terminator -- so it's parsed below as a value.
181
+ return [header_len, true, nil, nil, nil] if no_body?(tokens, body_len)
180
182
 
181
183
  resp_size = header_len + body_len + TERMINATOR.length
182
184
  # The header is in the buffer, but the body is not. As we don't have
@@ -189,6 +191,11 @@ module Dalli
189
191
  full_response_from_buffer(tokens, body, resp_size)
190
192
  end
191
193
 
194
+ # A zero-size reply has no body unless it's a VA (a hit on an empty value)
195
+ def no_body?(tokens, body_len)
196
+ body_len.zero? && tokens.first != VA
197
+ end
198
+
192
199
  def error_on_unexpected!(expected_codes)
193
200
  tokens = next_line_to_tokens
194
201
 
@@ -146,7 +146,7 @@ module Dalli
146
146
  response_processor.meta_set_with_cas unless quiet?
147
147
  end
148
148
 
149
- # rubocop:disable Metrics/ParameterLists
149
+ # rubocop:disable-next Metrics/ParameterLists
150
150
  def write_storage_req(mode, key, raw_value, ttl = nil, cas = nil, options = {}, quiet: quiet?)
151
151
  (value, bitflags) = @value_marshaller.store(key, raw_value, options)
152
152
  ttl = TtlSanitizer.sanitize(ttl) if ttl
@@ -159,7 +159,6 @@ module Dalli
159
159
  write(TERMINATOR)
160
160
  @connection_manager.flush unless quiet
161
161
  end
162
- # rubocop:enable Metrics/ParameterLists
163
162
 
164
163
  def append(key, value)
165
164
  write_append_prepend_req(:append, key, value)
@@ -171,7 +170,7 @@ module Dalli
171
170
  response_processor.meta_set_append_prepend unless quiet?
172
171
  end
173
172
 
174
- # rubocop:disable Metrics/ParameterLists
173
+ # rubocop:disable-next Metrics/ParameterLists
175
174
  def write_append_prepend_req(mode, key, value, ttl = nil, cas = nil, _options = {})
176
175
  ttl = TtlSanitizer.sanitize(ttl) if ttl
177
176
  encoded_key, base64 = KeyRegularizer.encode(key)
@@ -182,7 +181,6 @@ module Dalli
182
181
  write(TERMINATOR)
183
182
  @connection_manager.flush unless quiet?
184
183
  end
185
- # rubocop:enable Metrics/ParameterLists
186
184
 
187
185
  # Delete Commands
188
186
  def delete(key, cas)
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '4.3.3'
4
+ VERSION = '4.3.5'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
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: 4.3.3
4
+ version: 4.3.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
@@ -93,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
93
93
  - !ruby/object:Gem::Version
94
94
  version: '0'
95
95
  requirements: []
96
- rubygems_version: 4.0.6
96
+ rubygems_version: 4.0.21
97
97
  specification_version: 4
98
98
  summary: High performance memcached client for Ruby
99
99
  test_files: []