rack-session 1.0.2 → 2.1.2
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/lib/rack/session/abstract/id.rb +535 -0
- data/lib/rack/session/constants.rb +13 -0
- data/lib/rack/session/cookie.rb +315 -0
- data/lib/rack/session/encryptor.rb +415 -0
- data/lib/rack/session/pool.rb +82 -0
- data/lib/rack/session/version.rb +1 -1
- data/lib/rack/session.rb +12 -5
- data/license.md +50 -2
- data/releases.md +31 -0
- metadata +30 -14
|
@@ -0,0 +1,415 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2022-2023, by Samuel Williams.
|
|
5
|
+
# Copyright, 2022, by Philip Arndt.
|
|
6
|
+
|
|
7
|
+
require 'base64'
|
|
8
|
+
require 'json'
|
|
9
|
+
require 'openssl'
|
|
10
|
+
require 'securerandom'
|
|
11
|
+
|
|
12
|
+
require 'rack/utils'
|
|
13
|
+
|
|
14
|
+
module Rack
|
|
15
|
+
module Session
|
|
16
|
+
class Encryptor
|
|
17
|
+
class Error < StandardError
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class InvalidSignature < Error
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class InvalidMessage < Error
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
module Serializable
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
# Returns a serialized payload of the message. If a :pad_size is supplied,
|
|
30
|
+
# the message will be padded. The first 2 bytes of the returned string will
|
|
31
|
+
# indicating the amount of padding.
|
|
32
|
+
def serialize_payload(message)
|
|
33
|
+
serialized_data = serializer.dump(message)
|
|
34
|
+
|
|
35
|
+
return "#{[0].pack('v')}#{serialized_data.force_encoding(Encoding::BINARY)}" if @options[:pad_size].nil?
|
|
36
|
+
|
|
37
|
+
padding_bytes = @options[:pad_size] - (2 + serialized_data.size) % @options[:pad_size]
|
|
38
|
+
padding_data = SecureRandom.random_bytes(padding_bytes)
|
|
39
|
+
|
|
40
|
+
"#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data.force_encoding(Encoding::BINARY)}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Return the deserialized message. The first 2 bytes will be read as the
|
|
44
|
+
# amount of padding.
|
|
45
|
+
def deserialized_message(data)
|
|
46
|
+
# Read the first 2 bytes as the padding_bytes size
|
|
47
|
+
padding_bytes, = data.unpack('v')
|
|
48
|
+
|
|
49
|
+
# Slice out the serialized_data and deserialize it
|
|
50
|
+
serialized_data = data.slice(2 + padding_bytes, data.bytesize)
|
|
51
|
+
serializer.load serialized_data
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def serializer
|
|
55
|
+
@serializer ||= @options[:serialize_json] ? JSON : Marshal
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
class V1
|
|
60
|
+
include Serializable
|
|
61
|
+
|
|
62
|
+
# The secret String must be at least 64 bytes in size. The first 32 bytes
|
|
63
|
+
# will be used for the encryption cipher key. The remainder will be used
|
|
64
|
+
# for an HMAC key.
|
|
65
|
+
#
|
|
66
|
+
# Options may include:
|
|
67
|
+
# * :serialize_json
|
|
68
|
+
# Use JSON for message serialization instead of Marshal. This can be
|
|
69
|
+
# viewed as a security enhancement.
|
|
70
|
+
# * :pad_size
|
|
71
|
+
# Pad encrypted message data, to a multiple of this many bytes
|
|
72
|
+
# (default: 32). This can be between 2-4096 bytes, or +nil+ to disable
|
|
73
|
+
# padding.
|
|
74
|
+
# * :purpose
|
|
75
|
+
# Limit messages to a specific purpose. This can be viewed as a
|
|
76
|
+
# security enhancement to prevent message reuse from different contexts
|
|
77
|
+
# if keys are reused.
|
|
78
|
+
#
|
|
79
|
+
# Cryptography and Output Format:
|
|
80
|
+
#
|
|
81
|
+
# urlsafe_encode64(version + random_data + IV + encrypted data + HMAC)
|
|
82
|
+
#
|
|
83
|
+
# Where:
|
|
84
|
+
# * version - 1 byte with value 0x01
|
|
85
|
+
# * random_data - 32 bytes used for generating the per-message secret
|
|
86
|
+
# * IV - 16 bytes random initialization vector
|
|
87
|
+
# * HMAC - 32 bytes HMAC-SHA-256 of all preceding data, plus the purpose
|
|
88
|
+
# value
|
|
89
|
+
def initialize(secret, opts = {})
|
|
90
|
+
raise ArgumentError, 'secret must be a String' unless secret.is_a?(String)
|
|
91
|
+
raise ArgumentError, "invalid secret: #{secret.bytesize}, must be >=64" unless secret.bytesize >= 64
|
|
92
|
+
|
|
93
|
+
case opts[:pad_size]
|
|
94
|
+
when nil
|
|
95
|
+
# padding is disabled
|
|
96
|
+
when Integer
|
|
97
|
+
raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}" unless (2..4096).include? opts[:pad_size]
|
|
98
|
+
else
|
|
99
|
+
raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}; must be Integer or nil"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
@options = {
|
|
103
|
+
serialize_json: false, pad_size: 32, purpose: nil
|
|
104
|
+
}.update(opts)
|
|
105
|
+
|
|
106
|
+
@hmac_secret = secret.dup.force_encoding(Encoding::BINARY)
|
|
107
|
+
@cipher_secret = @hmac_secret.slice!(0, 32)
|
|
108
|
+
|
|
109
|
+
@hmac_secret.freeze
|
|
110
|
+
@cipher_secret.freeze
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def decrypt(base64_data)
|
|
114
|
+
data = Base64.urlsafe_decode64(base64_data)
|
|
115
|
+
|
|
116
|
+
signature = data.slice!(-32..-1)
|
|
117
|
+
verify_authenticity!(data, signature)
|
|
118
|
+
|
|
119
|
+
version = data.slice!(0, 1)
|
|
120
|
+
raise InvalidMessage, 'wrong version' unless version == "\1"
|
|
121
|
+
|
|
122
|
+
message_secret = data.slice!(0, 32)
|
|
123
|
+
cipher_iv = data.slice!(0, 16)
|
|
124
|
+
|
|
125
|
+
cipher = new_cipher
|
|
126
|
+
cipher.decrypt
|
|
127
|
+
|
|
128
|
+
set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
|
|
129
|
+
|
|
130
|
+
cipher.iv = cipher_iv
|
|
131
|
+
data = cipher.update(data) << cipher.final
|
|
132
|
+
|
|
133
|
+
deserialized_message data
|
|
134
|
+
rescue ArgumentError
|
|
135
|
+
raise InvalidSignature, 'Message invalid'
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def encrypt(message)
|
|
139
|
+
version = "\1"
|
|
140
|
+
|
|
141
|
+
serialized_payload = serialize_payload(message)
|
|
142
|
+
message_secret, cipher_secret = new_message_and_cipher_secret
|
|
143
|
+
|
|
144
|
+
cipher = new_cipher
|
|
145
|
+
cipher.encrypt
|
|
146
|
+
|
|
147
|
+
set_cipher_key(cipher, cipher_secret)
|
|
148
|
+
|
|
149
|
+
cipher_iv = cipher.random_iv
|
|
150
|
+
|
|
151
|
+
encrypted_data = cipher.update(serialized_payload) << cipher.final
|
|
152
|
+
|
|
153
|
+
data = String.new
|
|
154
|
+
data << version
|
|
155
|
+
data << message_secret
|
|
156
|
+
data << cipher_iv
|
|
157
|
+
data << encrypted_data
|
|
158
|
+
data << compute_signature(data)
|
|
159
|
+
|
|
160
|
+
Base64.urlsafe_encode64(data)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def new_cipher
|
|
166
|
+
OpenSSL::Cipher.new('aes-256-ctr')
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def new_message_and_cipher_secret
|
|
170
|
+
message_secret = SecureRandom.random_bytes(32)
|
|
171
|
+
|
|
172
|
+
[message_secret, cipher_secret_from_message_secret(message_secret)]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def cipher_secret_from_message_secret(message_secret)
|
|
176
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), @cipher_secret, message_secret)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def set_cipher_key(cipher, key)
|
|
180
|
+
cipher.key = key
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def compute_signature(data)
|
|
184
|
+
signing_data = data
|
|
185
|
+
signing_data += @options[:purpose] if @options[:purpose]
|
|
186
|
+
|
|
187
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), @hmac_secret, signing_data)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def verify_authenticity!(data, signature)
|
|
191
|
+
raise InvalidMessage, 'Message is invalid' if data.nil? || signature.nil?
|
|
192
|
+
|
|
193
|
+
unless Rack::Utils.secure_compare(signature, compute_signature(data))
|
|
194
|
+
raise InvalidSignature, 'HMAC is invalid'
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
class V2
|
|
200
|
+
include Serializable
|
|
201
|
+
|
|
202
|
+
# The secret String must be at least 32 bytes in size.
|
|
203
|
+
#
|
|
204
|
+
# Options may include:
|
|
205
|
+
# * :pad_size
|
|
206
|
+
# Pad encrypted message data, to a multiple of this many bytes
|
|
207
|
+
# (default: 32). This can be between 2-4096 bytes, or +nil+ to disable
|
|
208
|
+
# padding.
|
|
209
|
+
# * :purpose
|
|
210
|
+
# Limit messages to a specific purpose. This can be viewed as a
|
|
211
|
+
# security enhancement to prevent message reuse from different contexts
|
|
212
|
+
# if keys are reused.
|
|
213
|
+
#
|
|
214
|
+
# Cryptography and Output Format:
|
|
215
|
+
#
|
|
216
|
+
# strict_encode64(version + salt + IV + authentication tag + ciphertext)
|
|
217
|
+
#
|
|
218
|
+
# Where:
|
|
219
|
+
# * version - 1 byte with value 0x02
|
|
220
|
+
# * salt - 32 bytes used for generating the per-message secret
|
|
221
|
+
# * IV - 12 bytes random initialization vector
|
|
222
|
+
# * authentication tag - 16 bytes authentication tag generated by the GCM mode, covering version and salt
|
|
223
|
+
#
|
|
224
|
+
# Considerations about V2:
|
|
225
|
+
#
|
|
226
|
+
# 1) It uses non URL-safe Base64 encoding as it's faster than its
|
|
227
|
+
# URL-safe counterpart - as of Ruby 3.2, Base64.urlsafe_encode64 is
|
|
228
|
+
# roughly equivalent to
|
|
229
|
+
#
|
|
230
|
+
# Base64.strict_encode64(data).tr("-_", "+/")
|
|
231
|
+
#
|
|
232
|
+
# - and cookie values don't need to be URL-safe.
|
|
233
|
+
def initialize(secret, opts = {})
|
|
234
|
+
raise ArgumentError, 'secret must be a String' unless secret.is_a?(String)
|
|
235
|
+
|
|
236
|
+
unless secret.bytesize >= 32
|
|
237
|
+
raise ArgumentError, "invalid secret: it's #{secret.bytesize}-byte long, must be >=32"
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
case opts[:pad_size]
|
|
241
|
+
when nil
|
|
242
|
+
# padding is disabled
|
|
243
|
+
when Integer
|
|
244
|
+
raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}" unless (2..4096).include? opts[:pad_size]
|
|
245
|
+
else
|
|
246
|
+
raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}; must be Integer or nil"
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
@options = {
|
|
250
|
+
serialize_json: false, pad_size: 32, purpose: nil
|
|
251
|
+
}.update(opts)
|
|
252
|
+
|
|
253
|
+
@cipher_secret = secret.dup.force_encoding(Encoding::BINARY).slice!(0, 32)
|
|
254
|
+
@cipher_secret.freeze
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
def decrypt(base64_data)
|
|
258
|
+
data = Base64.strict_decode64(base64_data)
|
|
259
|
+
if data.bytesize <= 61 # version + salt + iv + auth_tag = 61 byte (and we also need some ciphertext :)
|
|
260
|
+
raise InvalidMessage, 'invalid message'
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
version = data[0]
|
|
264
|
+
raise InvalidMessage, 'invalid message' unless version == "\2"
|
|
265
|
+
|
|
266
|
+
ciphertext = data.slice!(61..-1)
|
|
267
|
+
auth_tag = data.slice!(45, 16)
|
|
268
|
+
cipher_iv = data.slice!(33, 12)
|
|
269
|
+
|
|
270
|
+
cipher = new_cipher
|
|
271
|
+
cipher.decrypt
|
|
272
|
+
salt = data.slice(1, 32)
|
|
273
|
+
set_cipher_key(cipher, message_secret_from_salt(salt))
|
|
274
|
+
cipher.iv = cipher_iv
|
|
275
|
+
cipher.auth_tag = auth_tag
|
|
276
|
+
cipher.auth_data = (purpose = @options[:purpose]) ? data + purpose : data
|
|
277
|
+
|
|
278
|
+
plaintext = cipher.update(ciphertext) << cipher.final
|
|
279
|
+
|
|
280
|
+
deserialized_message plaintext
|
|
281
|
+
rescue ArgumentError, OpenSSL::Cipher::CipherError
|
|
282
|
+
raise InvalidSignature, 'invalid message'
|
|
283
|
+
end
|
|
284
|
+
|
|
285
|
+
def encrypt(message)
|
|
286
|
+
version = "\2"
|
|
287
|
+
|
|
288
|
+
serialized_payload = serialize_payload(message)
|
|
289
|
+
|
|
290
|
+
cipher = new_cipher
|
|
291
|
+
cipher.encrypt
|
|
292
|
+
salt, message_secret = new_salt_and_message_secret
|
|
293
|
+
set_cipher_key(cipher, message_secret)
|
|
294
|
+
cipher.iv_len = 12
|
|
295
|
+
cipher_iv = cipher.random_iv
|
|
296
|
+
|
|
297
|
+
data = String.new
|
|
298
|
+
data << version
|
|
299
|
+
data << salt
|
|
300
|
+
|
|
301
|
+
cipher.auth_data = (purpose = @options[:purpose]) ? data + purpose : data
|
|
302
|
+
encrypted_data = cipher.update(serialized_payload) << cipher.final
|
|
303
|
+
|
|
304
|
+
data << cipher_iv
|
|
305
|
+
data << auth_tag_from(cipher)
|
|
306
|
+
data << encrypted_data
|
|
307
|
+
|
|
308
|
+
Base64.strict_encode64(data)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
private
|
|
312
|
+
|
|
313
|
+
def new_cipher
|
|
314
|
+
OpenSSL::Cipher.new('aes-256-gcm')
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def new_salt_and_message_secret
|
|
318
|
+
salt = SecureRandom.random_bytes(32)
|
|
319
|
+
|
|
320
|
+
[salt, message_secret_from_salt(salt)]
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def message_secret_from_salt(salt)
|
|
324
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new('SHA256'), @cipher_secret, salt)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def set_cipher_key(cipher, key)
|
|
328
|
+
cipher.key = key
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
if RUBY_ENGINE == 'jruby'
|
|
332
|
+
# JRuby's OpenSSL implementation doesn't currently support passing
|
|
333
|
+
# an argument to #auth_tag. Here we work around that.
|
|
334
|
+
def auth_tag_from(cipher)
|
|
335
|
+
tag = cipher.auth_tag
|
|
336
|
+
raise Error, 'the auth tag must be 16 bytes long' if tag.bytesize != 16
|
|
337
|
+
|
|
338
|
+
tag
|
|
339
|
+
end
|
|
340
|
+
else
|
|
341
|
+
def auth_tag_from(cipher)
|
|
342
|
+
cipher.auth_tag(16)
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def initialize(secret, opts = {})
|
|
348
|
+
opts = opts.dup
|
|
349
|
+
|
|
350
|
+
@mode = opts.delete(:mode)&.to_sym || :guess_version
|
|
351
|
+
case @mode
|
|
352
|
+
when :v1
|
|
353
|
+
@v1 = V1.new(secret, opts)
|
|
354
|
+
when :v2
|
|
355
|
+
@v2 = V2.new(secret, opts)
|
|
356
|
+
else
|
|
357
|
+
@v1 = V1.new(secret, opts)
|
|
358
|
+
@v2 = V2.new(secret, opts)
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def decrypt(base64_data)
|
|
363
|
+
decryptor =
|
|
364
|
+
case @mode
|
|
365
|
+
when :v2
|
|
366
|
+
v2
|
|
367
|
+
when :v1
|
|
368
|
+
v1
|
|
369
|
+
else
|
|
370
|
+
guess_decryptor(base64_data)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
decryptor.decrypt(base64_data)
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def encrypt(message)
|
|
377
|
+
encryptor =
|
|
378
|
+
case @mode
|
|
379
|
+
when :v1
|
|
380
|
+
v1
|
|
381
|
+
else
|
|
382
|
+
v2
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
encryptor.encrypt(message)
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
private
|
|
389
|
+
|
|
390
|
+
attr_reader :v1, :v2
|
|
391
|
+
|
|
392
|
+
def guess_decryptor(base64_data)
|
|
393
|
+
raise InvalidMessage, 'invalid message' if base64_data.nil? || base64_data.bytesize < 4
|
|
394
|
+
|
|
395
|
+
first_encoded_4_bytes = base64_data.slice(0, 4)
|
|
396
|
+
# Transform the 4 bytes into non-URL-safe base64-encoded data. Nothing
|
|
397
|
+
# happens if the data is already non-URL-safe base64.
|
|
398
|
+
first_encoded_4_bytes.tr!('-_', '+/')
|
|
399
|
+
first_decoded_3_bytes = Base64.strict_decode64(first_encoded_4_bytes)
|
|
400
|
+
|
|
401
|
+
version = first_decoded_3_bytes[0]
|
|
402
|
+
case version
|
|
403
|
+
when "\2"
|
|
404
|
+
v2
|
|
405
|
+
when "\1"
|
|
406
|
+
v1
|
|
407
|
+
else
|
|
408
|
+
raise InvalidMessage, 'invalid message'
|
|
409
|
+
end
|
|
410
|
+
rescue ArgumentError
|
|
411
|
+
raise InvalidMessage, 'invalid message'
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2022-2023, by Samuel Williams.
|
|
5
|
+
|
|
6
|
+
require_relative 'abstract/id'
|
|
7
|
+
|
|
8
|
+
module Rack
|
|
9
|
+
module Session
|
|
10
|
+
# Rack::Session::Pool provides simple cookie based session management.
|
|
11
|
+
# Session data is stored in a hash held by @pool.
|
|
12
|
+
# In the context of a multithreaded environment, sessions being
|
|
13
|
+
# committed to the pool is done in a merging manner.
|
|
14
|
+
#
|
|
15
|
+
# The :drop option is available in rack.session.options if you wish to
|
|
16
|
+
# explicitly remove the session from the session cache.
|
|
17
|
+
#
|
|
18
|
+
# Example:
|
|
19
|
+
# myapp = MyRackApp.new
|
|
20
|
+
# sessioned = Rack::Session::Pool.new(myapp,
|
|
21
|
+
# :domain => 'foo.com',
|
|
22
|
+
# :expire_after => 2592000
|
|
23
|
+
# )
|
|
24
|
+
# Rack::Handler::WEBrick.run sessioned
|
|
25
|
+
|
|
26
|
+
class Pool < Abstract::PersistedSecure
|
|
27
|
+
attr_reader :mutex, :pool
|
|
28
|
+
DEFAULT_OPTIONS = Abstract::ID::DEFAULT_OPTIONS.merge(drop: false, allow_fallback: true)
|
|
29
|
+
|
|
30
|
+
def initialize(app, options = {})
|
|
31
|
+
super
|
|
32
|
+
@pool = Hash.new
|
|
33
|
+
@mutex = Mutex.new
|
|
34
|
+
@allow_fallback = @default_options.delete(:allow_fallback)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def generate_sid(*args, use_mutex: true)
|
|
38
|
+
loop do
|
|
39
|
+
sid = super(*args)
|
|
40
|
+
break sid unless use_mutex ? @mutex.synchronize { @pool.key? sid.private_id } : @pool.key?(sid.private_id)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def find_session(req, sid)
|
|
45
|
+
@mutex.synchronize do
|
|
46
|
+
unless sid and session = get_session_with_fallback(sid)
|
|
47
|
+
sid, session = generate_sid(use_mutex: false), {}
|
|
48
|
+
@pool.store sid.private_id, session
|
|
49
|
+
end
|
|
50
|
+
[sid, session]
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def write_session(req, session_id, new_session, options)
|
|
55
|
+
@mutex.synchronize do
|
|
56
|
+
return false unless get_session_with_fallback(session_id)
|
|
57
|
+
@pool.store session_id.private_id, new_session
|
|
58
|
+
session_id
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def delete_session(req, session_id, options)
|
|
63
|
+
@mutex.synchronize do
|
|
64
|
+
@pool.delete(session_id.public_id)
|
|
65
|
+
@pool.delete(session_id.private_id)
|
|
66
|
+
|
|
67
|
+
unless options[:drop]
|
|
68
|
+
sid = generate_sid(use_mutex: false)
|
|
69
|
+
@pool.store(sid.private_id, {})
|
|
70
|
+
sid
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def get_session_with_fallback(sid)
|
|
78
|
+
@pool[sid.private_id] || (@pool[sid.public_id] if @allow_fallback)
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
data/lib/rack/session/version.rb
CHANGED
data/lib/rack/session.rb
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
|
-
#
|
|
2
|
-
|
|
3
|
-
#
|
|
4
|
-
# -
|
|
5
|
-
#
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Released under the MIT License.
|
|
4
|
+
# Copyright, 2022-2023, by Samuel Williams.
|
|
5
|
+
# Copyright, 2022, by Jeremy Evans.
|
|
6
|
+
|
|
7
|
+
module Rack
|
|
8
|
+
module Session
|
|
9
|
+
autoload :Cookie, "rack/session/cookie"
|
|
10
|
+
autoload :Pool, "rack/session/pool"
|
|
11
|
+
end
|
|
12
|
+
end
|
data/license.md
CHANGED
|
@@ -1,7 +1,55 @@
|
|
|
1
1
|
# MIT License
|
|
2
2
|
|
|
3
|
-
Copyright,
|
|
4
|
-
Copyright,
|
|
3
|
+
Copyright, 2007-2008, by Leah Neukirchen.
|
|
4
|
+
Copyright, 2007-2009, by Scytrin dai Kinthra.
|
|
5
|
+
Copyright, 2008, by Daniel Roethlisberger.
|
|
6
|
+
Copyright, 2009, by Joshua Peek.
|
|
7
|
+
Copyright, 2009, by Mickaël Riga.
|
|
8
|
+
Copyright, 2010, by Simon Chiang.
|
|
9
|
+
Copyright, 2010-2011, by José Valim.
|
|
10
|
+
Copyright, 2010-2013, by James Tucker.
|
|
11
|
+
Copyright, 2010-2019, by Aaron Patterson.
|
|
12
|
+
Copyright, 2011, by Max Cantor.
|
|
13
|
+
Copyright, 2011-2012, by Konstantin Haase.
|
|
14
|
+
Copyright, 2011, by Will Leinweber.
|
|
15
|
+
Copyright, 2011, by John Manoogian III.
|
|
16
|
+
Copyright, 2012, by Yun Huang Yong.
|
|
17
|
+
Copyright, 2012, by Ravil Bayramgalin.
|
|
18
|
+
Copyright, 2012, by Timothy Elliott.
|
|
19
|
+
Copyright, 2012, by Jamie Macey.
|
|
20
|
+
Copyright, 2012-2015, by Santiago Pastorino.
|
|
21
|
+
Copyright, 2013, by Andrew Cole.
|
|
22
|
+
Copyright, 2013, by Postmodern.
|
|
23
|
+
Copyright, 2013, by Vipul A M.
|
|
24
|
+
Copyright, 2013, by Charles Hornberger.
|
|
25
|
+
Copyright, 2014, by Michal Bryxí.
|
|
26
|
+
Copyright, 2015, by deepj.
|
|
27
|
+
Copyright, 2015, by Doug McInnes.
|
|
28
|
+
Copyright, 2015, by David Runger.
|
|
29
|
+
Copyright, 2015, by Francesco Rodríguez.
|
|
30
|
+
Copyright, 2015, by Yuichiro Kaneko.
|
|
31
|
+
Copyright, 2015, by Michael Sauter.
|
|
32
|
+
Copyright, 2016, by Kir Shatrov.
|
|
33
|
+
Copyright, 2016, by Yann Vanhalewyn.
|
|
34
|
+
Copyright, 2016, by Jian Weihang.
|
|
35
|
+
Copyright, 2017, by Jordan Raine.
|
|
36
|
+
Copyright, 2018, by Dillon Welch.
|
|
37
|
+
Copyright, 2018, by Yoshiyuki Hirano.
|
|
38
|
+
Copyright, 2019, by Krzysztof Rybka.
|
|
39
|
+
Copyright, 2019, by Frederick Cheung.
|
|
40
|
+
Copyright, 2019, by Adrian Setyadi.
|
|
41
|
+
Copyright, 2019, by Rafael Mendonça França.
|
|
42
|
+
Copyright, 2019-2020, by Pavel Rosicky.
|
|
43
|
+
Copyright, 2019, by Dima Fatko.
|
|
44
|
+
Copyright, 2019, by Oleh Demianiuk.
|
|
45
|
+
Copyright, 2020-2023, by Samuel Williams.
|
|
46
|
+
Copyright, 2020-2022, by Jeremy Evans.
|
|
47
|
+
Copyright, 2020, by Alex Speller.
|
|
48
|
+
Copyright, 2020, by Ryuta Kamizono.
|
|
49
|
+
Copyright, 2020, by Yudai Suzuki.
|
|
50
|
+
Copyright, 2020, by Bart de Water.
|
|
51
|
+
Copyright, 2020, by Alec Clarke.
|
|
52
|
+
Copyright, 2021, by Michael Coyne.
|
|
5
53
|
Copyright, 2022, by Philip Arndt.
|
|
6
54
|
Copyright, 2022, by Jon Dufresne.
|
|
7
55
|
|
data/releases.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Releases
|
|
2
|
+
|
|
3
|
+
## v2.1.2
|
|
4
|
+
|
|
5
|
+
- [CVE-2026-39324](https://github.com/advisories/GHSA-33qg-7wpp-89cq) Don't fall back to unencrypted coder if encryptors are present.
|
|
6
|
+
|
|
7
|
+
## v2.1.1
|
|
8
|
+
|
|
9
|
+
- Prevent `Rack::Session::Pool` from recreating deleted sessions [CVE-2025-46336](https://github.com/rack/rack-session/security/advisories/GHSA-9j94-67jr-4cqj).
|
|
10
|
+
|
|
11
|
+
## v2.1.0
|
|
12
|
+
|
|
13
|
+
- Improved compatibility with Ruby 3.3+ and Rack 3+.
|
|
14
|
+
- Add support for cookie option `partitioned`.
|
|
15
|
+
- Introduce `assume_ssl` option to allow secure session cookies through insecure proxy.
|
|
16
|
+
|
|
17
|
+
## v2.0.0
|
|
18
|
+
|
|
19
|
+
- Initial migration of code from Rack 2, for Rack 3 release.
|
|
20
|
+
|
|
21
|
+
## v1.0.2
|
|
22
|
+
|
|
23
|
+
- Fix missing `rack/session.rb` file.
|
|
24
|
+
|
|
25
|
+
## v1.0.1
|
|
26
|
+
|
|
27
|
+
- Pin to `rack < 3`.
|
|
28
|
+
|
|
29
|
+
## v1.0.0
|
|
30
|
+
|
|
31
|
+
- Empty shim release for Rack 2.
|