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 +4 -4
- data/CHANGELOG.md +30 -0
- data/Gemfile +1 -1
- data/lib/dalli/client.rb +17 -3
- data/lib/dalli/protocol/base.rb +1 -2
- data/lib/dalli/protocol/binary/request_formatter.rb +1 -2
- data/lib/dalli/protocol/binary.rb +1 -2
- data/lib/dalli/protocol/meta/request_formatter.rb +21 -8
- data/lib/dalli/protocol/meta/response_processor.rb +9 -2
- data/lib/dalli/protocol/meta.rb +2 -4
- data/lib/dalli/version.rb +1 -1
- data/lib/rack/session/dalli.rb +1 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96ac26bd7ef748d2407efaa5d1bcfe8f762c2eb47d67f85fb6e19f789926abae
|
|
4
|
+
data.tar.gz: 617744189a342d6b39bd741a685b997bb0fbbae82afb3aeaafbf87ac79f6fe29
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
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
|
data/lib/dalli/protocol/base.rb
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
data/lib/dalli/protocol/meta.rb
CHANGED
|
@@ -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
data/lib/rack/session/dalli.rb
CHANGED
|
@@ -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.
|
|
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.
|
|
96
|
+
rubygems_version: 4.0.21
|
|
97
97
|
specification_version: 4
|
|
98
98
|
summary: High performance memcached client for Ruby
|
|
99
99
|
test_files: []
|