dalli 3.2.7 → 3.2.9

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: a98f25c5f6f750d72b83fdd0b6e45a9d693520a91b9b7d985bd26fd0abfac41e
4
- data.tar.gz: 9e95098e69ece841757034d69cc2091e0b450feed17290bf5fffd527e6d45ac7
3
+ metadata.gz: 2fe56d2ea432b05cde81f7b4c5e3017fc8e11788526cd3dbecbaede2e11c1659
4
+ data.tar.gz: 450380fe328c475a93b32d4b1cca10fef1d33065d13bed32e1126276621c2e54
5
5
  SHA512:
6
- metadata.gz: 25368bee67a5fd00aa7795cc06839c366fb7c1c33386ca0304c1cdfee3a4c82729716128d8f452cb7e0c0e2c24e2c3aec384eb7e3e0ee68c3915d93e17917456
7
- data.tar.gz: f0094059b3e7430b3b03a0a6d8795ba0e76bdf1622ba2531b3ceffa11ebb3fb2442a4512a5c98e27e00fba253a7241fb58f47e3f93c58944676c8cd5d0dfe123
6
+ metadata.gz: ccdbfa346efdf6a09ab53fbd4e44179b298028e348716780b5356d909270fce2fa5fdf66f00579703ab9e0a15dde4b699faf483b5fadb016cff434b85b99011a
7
+ data.tar.gz: 3c312e4303bf6b076b20fa28a73277b3975396af12cbee42e3c1cceaab9fde3cb679422f3301a0c3c06a784f3706c704d1e181207bb107b245db2683d7f0c690
data/CHANGELOG.md CHANGED
@@ -4,6 +4,25 @@ Dalli Changelog
4
4
  Unreleased
5
5
  ==========
6
6
 
7
+ 3.2.9
8
+ ==========
9
+
10
+ Security:
11
+
12
+ - Fix memcached command injection through the `incr`/`decr` default with the meta protocol (GHSA-6wmv-xq9m-fmp7)
13
+ - With `protocol: :meta`, the `default` argument of `incr`/`decr` was 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
14
+ - `default` must now be an Integer, or a String of decimal digits; anything else raises `ArgumentError` before a request is sent
15
+ - As defense in depth, the meta `RequestFormatter` now converts every numeric flag it writes (`D`, `J`, `N`, `T`) to an Integer
16
+ - Thanks to oss-security-shop for the report
17
+
18
+ 3.2.8
19
+ ==========
20
+
21
+ - Handle IO::TimeoutError when establishing connection (eugeneius)
22
+ - Drop dependency on base64 gem (Earlopain)
23
+ - Address incompatibility with resolv-replace (y9v)
24
+ - Add rubygems.org metadata (m-nakamura145)
25
+
7
26
  3.2.7
8
27
  ==========
9
28
 
data/lib/dalli/client.rb CHANGED
@@ -257,6 +257,8 @@ module Dalli
257
257
  ##
258
258
  # Incr adds the given amount to the counter on the memcached server.
259
259
  # Amt must be a positive integer value.
260
+ # Default, if given, must be an Integer (or a String of decimal digits);
261
+ # anything else raises ArgumentError.
260
262
  #
261
263
  # If default is nil, the counter must already exist or the operation
262
264
  # will fail and will return nil. Otherwise this method will return
@@ -269,6 +271,7 @@ module Dalli
269
271
  # If the value already exists, it must have been set with raw: true
270
272
  def incr(key, amt = 1, ttl = nil, default = nil)
271
273
  check_positive!(amt)
274
+ validate_integer!(:default, default)
272
275
 
273
276
  perform(:incr, key, amt.to_i, ttl_or_default(ttl), default)
274
277
  end
@@ -276,6 +279,8 @@ module Dalli
276
279
  ##
277
280
  # Decr subtracts the given amount from the counter on the memcached server.
278
281
  # Amt must be a positive integer value.
282
+ # Default, if given, must be an Integer (or a String of decimal digits);
283
+ # anything else raises ArgumentError.
279
284
  #
280
285
  # memcached counters are unsigned and cannot hold negative values. Calling
281
286
  # decr on a counter which is 0 will just return 0.
@@ -291,6 +296,7 @@ module Dalli
291
296
  # If the value already exists, it must have been set with raw: true
292
297
  def decr(key, amt = 1, ttl = nil, default = nil)
293
298
  check_positive!(amt)
299
+ validate_integer!(:default, default)
294
300
 
295
301
  perform(:decr, key, amt.to_i, ttl_or_default(ttl), default)
296
302
  end
@@ -373,6 +379,14 @@ module Dalli
373
379
  raise ArgumentError, "Positive values only: #{amt}" if amt.negative?
374
380
  end
375
381
 
382
+ # Numeric arguments that become meta protocol flags (GHSA-6wmv-xq9m-fmp7).
383
+ # Checked before the request starts so a bad value raises a clean
384
+ # ArgumentError, instead of Protocol::Base#request logging it as
385
+ # unexpected and closing the connection.
386
+ def validate_integer!(name, value)
387
+ Dalli::Protocol::Meta::RequestFormatter.integer_flag(name, value) unless value.nil?
388
+ end
389
+
376
390
  def cas_core(key, always_set, ttl = nil, req_options = nil)
377
391
  (value, cas) = perform(:cas, key)
378
392
  return if value.nil? && !always_set
@@ -55,7 +55,7 @@ module Dalli
55
55
  @sock = memcached_socket
56
56
  @pid = PIDCache.pid
57
57
  @request_in_progress = false
58
- rescue SystemCallError, Timeout::Error, EOFError, SocketError => e
58
+ rescue SystemCallError, *TIMEOUT_ERRORS, EOFError, SocketError => e
59
59
  # SocketError = DNS resolution failure
60
60
  error_on_request!(e)
61
61
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'base64'
4
-
5
3
  module Dalli
6
4
  module Protocol
7
5
  class Meta
@@ -17,13 +15,15 @@ module Dalli
17
15
  def self.encode(key)
18
16
  return [key, false] if key.ascii_only? && !WHITESPACE.match(key)
19
17
 
20
- [Base64.strict_encode64(key), true]
18
+ strict_base64_encoded = [key].pack('m0')
19
+ [strict_base64_encoded, true]
21
20
  end
22
21
 
23
22
  def self.decode(encoded_key, base64_encoded)
24
23
  return encoded_key unless base64_encoded
25
24
 
26
- Base64.strict_decode64(encoded_key).force_encoding(Encoding::UTF_8)
25
+ strict_base64_decoded = encoded_key.unpack1('m0')
26
+ strict_base64_decoded.force_encoding(Encoding::UTF_8)
27
27
  end
28
28
  end
29
29
  end
@@ -21,7 +21,7 @@ module Dalli
21
21
  cmd << ' v f' if value
22
22
  cmd << ' c' if return_cas
23
23
  cmd << ' b' if base64
24
- cmd << " T#{ttl}" if ttl
24
+ cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
25
25
  cmd << ' k q s' if quiet # Return the key in the response if quiet
26
26
  cmd + TERMINATOR
27
27
  end
@@ -32,7 +32,7 @@ module Dalli
32
32
  cmd << ' b' if base64
33
33
  cmd << " F#{bitflags}" if bitflags
34
34
  cmd << cas_string(cas)
35
- cmd << " T#{ttl}" if ttl
35
+ cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
36
36
  cmd << " M#{mode_to_token(mode)}"
37
37
  cmd << ' q' if quiet
38
38
  cmd << TERMINATOR
@@ -44,7 +44,7 @@ module Dalli
44
44
  cmd = "md #{key}"
45
45
  cmd << ' b' if base64
46
46
  cmd << cas_string(cas)
47
- cmd << " T#{ttl}" if ttl
47
+ cmd << " T#{integer_flag(:ttl, ttl)}" if ttl
48
48
  cmd << ' q' if quiet
49
49
  cmd + TERMINATOR
50
50
  end
@@ -52,10 +52,10 @@ module Dalli
52
52
  def self.meta_arithmetic(key:, delta:, initial:, incr: true, cas: nil, ttl: nil, base64: false, quiet: false)
53
53
  cmd = "ma #{key} v"
54
54
  cmd << ' b' if base64
55
- cmd << " D#{delta}" if delta
56
- cmd << " J#{initial}" if initial
55
+ cmd << " D#{integer_flag(:delta, delta)}" if delta
56
+ cmd << " J#{integer_flag(:initial, initial)}" if initial
57
57
  # Always set a TTL if an initial value is specified
58
- cmd << " N#{ttl || 0}" if ttl || initial
58
+ cmd << " N#{ttl ? integer_flag(:ttl, ttl) : 0}" if ttl || initial
59
59
  cmd << cas_string(cas)
60
60
  cmd << ' q' if quiet
61
61
  cmd << " M#{incr ? 'I' : 'D'}"
@@ -104,6 +104,18 @@ module Dalli
104
104
  end
105
105
  # rubocop:enable Metrics/MethodLength
106
106
 
107
+ # Numeric flag values are written straight into the command line, so a
108
+ # value that isn't an integer -- e.g. a String carrying CRLF -- would let
109
+ # the caller inject further memcached commands (GHSA-6wmv-xq9m-fmp7).
110
+ # Converting to Integer means only digits ever reach the wire. Strings
111
+ # are parsed as base 10 so "010" means 10, as memcached would read it,
112
+ # rather than octal 8.
113
+ def self.integer_flag(name, value)
114
+ value.is_a?(String) ? Integer(value, 10) : Integer(value)
115
+ rescue ArgumentError, TypeError, FloatDomainError
116
+ raise ArgumentError, "#{name} must be an Integer, got #{value.inspect}"
117
+ end
118
+
107
119
  def self.cas_string(cas)
108
120
  cas = parse_to_64_bit_int(cas, nil)
109
121
  cas.nil? || cas.zero? ? '' : " C#{cas}"
data/lib/dalli/socket.rb CHANGED
@@ -88,22 +88,30 @@ module Dalli
88
88
  # options - supports enhanced logging in the case of a timeout
89
89
  attr_accessor :options
90
90
 
91
- if RUBY_VERSION >= '3.0'
92
- def self.open(host, port, options = {})
93
- sock = new(host, port, connect_timeout: options[:socket_timeout])
91
+ def self.open(host, port, options = {})
92
+ create_socket_with_timeout(host, port, options) do |sock|
94
93
  sock.options = { host: host, port: port }.merge(options)
95
94
  init_socket_options(sock, options)
96
95
 
97
96
  options[:ssl_context] ? wrapping_ssl_socket(sock, host, options[:ssl_context]) : sock
98
97
  end
99
- else
100
- def self.open(host, port, options = {})
98
+ end
99
+
100
+ def self.create_socket_with_timeout(host, port, options)
101
+ # Check that TCPSocket#initialize was not overwritten by resolv-replace gem
102
+ # (part of ruby standard library since 3.0.0, should be removed in 3.4.0),
103
+ # as it does not handle keyword arguments correctly.
104
+ # To check this we are using the fact that resolv-replace
105
+ # aliases TCPSocket#initialize method to #original_resolv_initialize.
106
+ # https://github.com/ruby/resolv-replace/blob/v0.1.1/lib/resolv-replace.rb#L21
107
+ if RUBY_VERSION >= '3.0' &&
108
+ !::TCPSocket.private_instance_methods.include?(:original_resolv_initialize)
109
+ sock = new(host, port, connect_timeout: options[:socket_timeout])
110
+ yield(sock)
111
+ else
101
112
  Timeout.timeout(options[:socket_timeout]) do
102
113
  sock = new(host, port)
103
- sock.options = { host: host, port: port }.merge(options)
104
- init_socket_options(sock, options)
105
-
106
- options[:ssl_context] ? wrapping_ssl_socket(sock, host, options[:ssl_context]) : sock
114
+ yield(sock)
107
115
  end
108
116
  end
109
117
  end
data/lib/dalli/version.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Dalli
4
- VERSION = '3.2.7'
4
+ VERSION = '3.2.9'
5
5
 
6
6
  MIN_SUPPORTED_MEMCACHED_VERSION = '1.4'
7
7
  end
metadata CHANGED
@@ -1,30 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dalli
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.2.7
4
+ version: 3.2.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter M. Goldstein
8
8
  - Mike Perham
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2024-01-28 00:00:00.000000000 Z
13
- dependencies:
14
- - !ruby/object:Gem::Dependency
15
- name: base64
16
- requirement: !ruby/object:Gem::Requirement
17
- requirements:
18
- - - ">="
19
- - !ruby/object:Gem::Version
20
- version: '0'
21
- type: :runtime
22
- prerelease: false
23
- version_requirements: !ruby/object:Gem::Requirement
24
- requirements:
25
- - - ">="
26
- - !ruby/object:Gem::Version
27
- version: '0'
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
+ dependencies: []
28
13
  description: High performance memcached client for Ruby
29
14
  email:
30
15
  - peter.m.goldstein@gmail.com
@@ -73,8 +58,9 @@ homepage: https://github.com/petergoldstein/dalli
73
58
  licenses:
74
59
  - MIT
75
60
  metadata:
61
+ bug_tracker_uri: https://github.com/petergoldstein/dalli/issues
62
+ changelog_uri: https://github.com/petergoldstein/dalli/blob/main/CHANGELOG.md
76
63
  rubygems_mfa_required: 'true'
77
- post_install_message:
78
64
  rdoc_options: []
79
65
  require_paths:
80
66
  - lib
@@ -89,8 +75,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
89
75
  - !ruby/object:Gem::Version
90
76
  version: '0'
91
77
  requirements: []
92
- rubygems_version: 3.5.5
93
- signing_key:
78
+ rubygems_version: 4.0.21
94
79
  specification_version: 4
95
80
  summary: High performance memcached client for Ruby
96
81
  test_files: []