rack-session 1.0.2 → 2.1.1

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: 11a5c280b017199d82d7ac8f5518048cedb48f1e0e01ee162cefe4a4751b1931
4
- data.tar.gz: 3e38b13932869caacf14361cb9921b6b084feb2393f6c945cf9884f9647ac445
3
+ metadata.gz: 24dbcb8931b1a26d39b7165f872231e9ce7f9006e8495616416faeb4538ed8e6
4
+ data.tar.gz: c26f979218fb4ac3b626d8638ee7b0e00183ac1acffdc1fb393fd85919e1cf46
5
5
  SHA512:
6
- metadata.gz: e5c519939f66107a12bc1d561329fd3c4df2ea085d099395b190a3dd7dde0455677ff1b31c55e09528abd7223e3058d27d8a7ef12a0a25cba2dc18bec6b80910
7
- data.tar.gz: 789a2af79f74672cac563fbc2e0d00163393499d491a02107390cca3fffa3d4d1f53e6d35a494b0b0b057a7ba3b2aee70d5d5c4d3b249b1f1c0e116ea98f372c
6
+ metadata.gz: e345e1424c6092e771a16f15fee04c19128dacfa50b586fbd7795ce1699fe50cbf2b7028624dc945e60a055f20c99c0d88c7c1eec729b13d527f3c3bcc7d6e6e
7
+ data.tar.gz: 8da88daf469c01ebeef2bb75b5ee04efc42e836807b3000b028fa2eb73f11db3585410321297e7607bd0e9413f768dab2a55e54f08e5326697e1d472f9363e6c
@@ -0,0 +1,535 @@
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
+ require 'time'
8
+ require 'securerandom'
9
+ require 'digest/sha2'
10
+
11
+ require 'rack/constants'
12
+ require 'rack/request'
13
+ require 'rack/response'
14
+
15
+ require_relative '../constants'
16
+
17
+ module Rack
18
+
19
+ module Session
20
+
21
+ class SessionId
22
+ ID_VERSION = 2
23
+
24
+ attr_reader :public_id
25
+
26
+ def initialize(public_id)
27
+ @public_id = public_id
28
+ end
29
+
30
+ def private_id
31
+ "#{ID_VERSION}::#{hash_sid(public_id)}"
32
+ end
33
+
34
+ alias :cookie_value :public_id
35
+ alias :to_s :public_id
36
+
37
+ def empty?; false; end
38
+ def inspect; public_id.inspect; end
39
+
40
+ private
41
+
42
+ def hash_sid(sid)
43
+ Digest::SHA256.hexdigest(sid)
44
+ end
45
+ end
46
+
47
+ module Abstract
48
+ # SessionHash is responsible to lazily load the session from store.
49
+
50
+ class SessionHash
51
+ include Enumerable
52
+ attr_writer :id
53
+
54
+ Unspecified = Object.new
55
+
56
+ def self.find(req)
57
+ req.get_header RACK_SESSION
58
+ end
59
+
60
+ def self.set(req, session)
61
+ req.set_header RACK_SESSION, session
62
+ end
63
+
64
+ def self.set_options(req, options)
65
+ req.set_header RACK_SESSION_OPTIONS, options.dup
66
+ end
67
+
68
+ def initialize(store, req)
69
+ @store = store
70
+ @req = req
71
+ @loaded = false
72
+ end
73
+
74
+ def id
75
+ return @id if @loaded or instance_variable_defined?(:@id)
76
+ @id = @store.send(:extract_session_id, @req)
77
+ end
78
+
79
+ def options
80
+ @req.session_options
81
+ end
82
+
83
+ def each(&block)
84
+ load_for_read!
85
+ @data.each(&block)
86
+ end
87
+
88
+ def [](key)
89
+ load_for_read!
90
+ @data[key.to_s]
91
+ end
92
+
93
+ def dig(key, *keys)
94
+ load_for_read!
95
+ @data.dig(key.to_s, *keys)
96
+ end
97
+
98
+ def fetch(key, default = Unspecified, &block)
99
+ load_for_read!
100
+ if default == Unspecified
101
+ @data.fetch(key.to_s, &block)
102
+ else
103
+ @data.fetch(key.to_s, default, &block)
104
+ end
105
+ end
106
+
107
+ def has_key?(key)
108
+ load_for_read!
109
+ @data.has_key?(key.to_s)
110
+ end
111
+ alias :key? :has_key?
112
+ alias :include? :has_key?
113
+
114
+ def []=(key, value)
115
+ load_for_write!
116
+ @data[key.to_s] = value
117
+ end
118
+ alias :store :[]=
119
+
120
+ def clear
121
+ load_for_write!
122
+ @data.clear
123
+ end
124
+
125
+ def destroy
126
+ clear
127
+ @id = @store.send(:delete_session, @req, id, options)
128
+ end
129
+
130
+ def to_hash
131
+ load_for_read!
132
+ @data.dup
133
+ end
134
+
135
+ def update(hash)
136
+ load_for_write!
137
+ @data.update(stringify_keys(hash))
138
+ end
139
+ alias :merge! :update
140
+
141
+ def replace(hash)
142
+ load_for_write!
143
+ @data.replace(stringify_keys(hash))
144
+ end
145
+
146
+ def delete(key)
147
+ load_for_write!
148
+ @data.delete(key.to_s)
149
+ end
150
+
151
+ def inspect
152
+ if loaded?
153
+ @data.inspect
154
+ else
155
+ "#<#{self.class}:0x#{self.object_id.to_s(16)} not yet loaded>"
156
+ end
157
+ end
158
+
159
+ def exists?
160
+ return @exists if instance_variable_defined?(:@exists)
161
+ @data = {}
162
+ @exists = @store.send(:session_exists?, @req)
163
+ end
164
+
165
+ def loaded?
166
+ @loaded
167
+ end
168
+
169
+ def empty?
170
+ load_for_read!
171
+ @data.empty?
172
+ end
173
+
174
+ def keys
175
+ load_for_read!
176
+ @data.keys
177
+ end
178
+
179
+ def values
180
+ load_for_read!
181
+ @data.values
182
+ end
183
+
184
+ private
185
+
186
+ def load_for_read!
187
+ load! if !loaded? && exists?
188
+ end
189
+
190
+ def load_for_write!
191
+ load! unless loaded?
192
+ end
193
+
194
+ def load!
195
+ @id, session = @store.send(:load_session, @req)
196
+ @data = stringify_keys(session)
197
+ @loaded = true
198
+ end
199
+
200
+ def stringify_keys(other)
201
+ # Use transform_keys after dropping Ruby 2.4 support
202
+ hash = {}
203
+ other.to_hash.each do |key, value|
204
+ hash[key.to_s] = value
205
+ end
206
+ hash
207
+ end
208
+ end
209
+
210
+ # ID sets up a basic framework for implementing an id based sessioning
211
+ # service. Cookies sent to the client for maintaining sessions will only
212
+ # contain an id reference. Only #find_session, #write_session and
213
+ # #delete_session are required to be overwritten.
214
+ #
215
+ # All parameters are optional.
216
+ # * :key determines the name of the cookie, by default it is
217
+ # 'rack.session'
218
+ # * :path, :domain, :expire_after, :secure, :httponly, :partitioned and :same_site set
219
+ # the related cookie options as by Rack::Response#set_cookie
220
+ # * :skip will not a set a cookie in the response nor update the session state
221
+ # * :defer will not set a cookie in the response but still update the session
222
+ # state if it is used with a backend
223
+ # * :renew (implementation dependent) will prompt the generation of a new
224
+ # session id, and migration of data to be referenced at the new id. If
225
+ # :defer is set, it will be overridden and the cookie will be set.
226
+ # * :sidbits sets the number of bits in length that a generated session
227
+ # id will be.
228
+ #
229
+ # These options can be set on a per request basis, at the location of
230
+ # <tt>env['rack.session.options']</tt>. Additionally the id of the
231
+ # session can be found within the options hash at the key :id. It is
232
+ # highly not recommended to change its value.
233
+ #
234
+ # Is Rack::Utils::Context compatible.
235
+ #
236
+ # Not included by default; you must require 'rack/session/abstract/id'
237
+ # to use.
238
+
239
+ class Persisted
240
+ DEFAULT_OPTIONS = {
241
+ key: RACK_SESSION,
242
+ path: '/',
243
+ domain: nil,
244
+ expire_after: nil,
245
+ secure: false,
246
+ httponly: true,
247
+ partitioned: false,
248
+ defer: false,
249
+ renew: false,
250
+ sidbits: 128,
251
+ cookie_only: true,
252
+ secure_random: ::SecureRandom
253
+ }.freeze
254
+
255
+ attr_reader :key, :default_options, :sid_secure, :same_site
256
+
257
+ def initialize(app, options = {})
258
+ @app = app
259
+ @default_options = self.class::DEFAULT_OPTIONS.merge(options)
260
+ @key = @default_options.delete(:key)
261
+ @assume_ssl = @default_options.delete(:assume_ssl)
262
+ @cookie_only = @default_options.delete(:cookie_only)
263
+ @same_site = @default_options.delete(:same_site)
264
+ initialize_sid
265
+ end
266
+
267
+ def call(env)
268
+ context(env)
269
+ end
270
+
271
+ def context(env, app = @app)
272
+ req = make_request env
273
+ prepare_session(req)
274
+ status, headers, body = app.call(req.env)
275
+ res = Rack::Response::Raw.new status, headers
276
+ commit_session(req, res)
277
+ [status, headers, body]
278
+ end
279
+
280
+ private
281
+
282
+ def make_request(env)
283
+ Rack::Request.new env
284
+ end
285
+
286
+ def initialize_sid
287
+ @sidbits = @default_options[:sidbits]
288
+ @sid_secure = @default_options[:secure_random]
289
+ @sid_length = @sidbits / 4
290
+ end
291
+
292
+ # Generate a new session id using Ruby #rand. The size of the
293
+ # session id is controlled by the :sidbits option.
294
+ # Monkey patch this to use custom methods for session id generation.
295
+
296
+ def generate_sid(secure = @sid_secure)
297
+ if secure
298
+ secure.hex(@sid_length)
299
+ else
300
+ "%0#{@sid_length}x" % Kernel.rand(2**@sidbits - 1)
301
+ end
302
+ rescue NotImplementedError
303
+ generate_sid(false)
304
+ end
305
+
306
+ # Sets the lazy session at 'rack.session' and places options and session
307
+ # metadata into 'rack.session.options'.
308
+
309
+ def prepare_session(req)
310
+ session_was = req.get_header RACK_SESSION
311
+ session = session_class.new(self, req)
312
+ req.set_header RACK_SESSION, session
313
+ req.set_header RACK_SESSION_OPTIONS, @default_options.dup
314
+ session.merge! session_was if session_was
315
+ end
316
+
317
+ # Extracts the session id from provided cookies and passes it and the
318
+ # environment to #find_session.
319
+
320
+ def load_session(req)
321
+ sid = current_session_id(req)
322
+ sid, session = find_session(req, sid)
323
+ [sid, session || {}]
324
+ end
325
+
326
+ # Extract session id from request object.
327
+
328
+ def extract_session_id(request)
329
+ sid = request.cookies[@key]
330
+ sid ||= request.params[@key] unless @cookie_only
331
+ sid
332
+ end
333
+
334
+ # Returns the current session id from the SessionHash.
335
+
336
+ def current_session_id(req)
337
+ req.get_header(RACK_SESSION).id
338
+ end
339
+
340
+ # Check if the session exists or not.
341
+
342
+ def session_exists?(req)
343
+ value = current_session_id(req)
344
+ value && !value.empty?
345
+ end
346
+
347
+ # Session should be committed if it was loaded, any of specific options like :renew, :drop
348
+ # or :expire_after was given and the security permissions match. Skips if skip is given.
349
+
350
+ def commit_session?(req, session, options)
351
+ if options[:skip]
352
+ false
353
+ else
354
+ has_session = loaded_session?(session) || forced_session_update?(session, options)
355
+ has_session && security_matches?(req, options)
356
+ end
357
+ end
358
+
359
+ def loaded_session?(session)
360
+ !session.is_a?(session_class) || session.loaded?
361
+ end
362
+
363
+ def forced_session_update?(session, options)
364
+ force_options?(options) && session && !session.empty?
365
+ end
366
+
367
+ def force_options?(options)
368
+ options.values_at(:max_age, :renew, :drop, :defer, :expire_after).any?
369
+ end
370
+
371
+ def security_matches?(request, options)
372
+ return true unless options[:secure]
373
+ request.ssl? || @assume_ssl == true
374
+ end
375
+
376
+ # Acquires the session from the environment and the session id from
377
+ # the session options and passes them to #write_session. If successful
378
+ # and the :defer option is not true, a cookie will be added to the
379
+ # response with the session's id.
380
+
381
+ def commit_session(req, res)
382
+ session = req.get_header RACK_SESSION
383
+ options = session.options
384
+
385
+ if options[:drop] || options[:renew]
386
+ session_id = delete_session(req, session.id || generate_sid, options)
387
+ return unless session_id
388
+ end
389
+
390
+ return unless commit_session?(req, session, options)
391
+
392
+ session.send(:load!) unless loaded_session?(session)
393
+ session_id ||= session.id
394
+ session_data = session.to_hash.delete_if { |k, v| v.nil? }
395
+
396
+ if not data = write_session(req, session_id, session_data, options)
397
+ req.get_header(RACK_ERRORS).puts("Warning! #{self.class.name} failed to save session. Content dropped.")
398
+ elsif options[:defer] and not options[:renew]
399
+ req.get_header(RACK_ERRORS).puts("Deferring cookie for #{session_id}") if $VERBOSE
400
+ else
401
+ cookie = Hash.new
402
+ cookie[:value] = cookie_value(data)
403
+ cookie[:expires] = Time.now + options[:expire_after] if options[:expire_after]
404
+ cookie[:expires] = Time.now + options[:max_age] if options[:max_age]
405
+
406
+ if @same_site.respond_to? :call
407
+ cookie[:same_site] = @same_site.call(req, res)
408
+ else
409
+ cookie[:same_site] = @same_site
410
+ end
411
+ set_cookie(req, res, cookie.merge!(options))
412
+ end
413
+ end
414
+ public :commit_session
415
+
416
+ def cookie_value(data)
417
+ data
418
+ end
419
+
420
+ # Sets the cookie back to the client with session id. We skip the cookie
421
+ # setting if the value didn't change (sid is the same) or expires was given.
422
+
423
+ def set_cookie(request, response, cookie)
424
+ if request.cookies[@key] != cookie[:value] || cookie[:expires]
425
+ response.set_cookie(@key, cookie)
426
+ end
427
+ end
428
+
429
+ # Allow subclasses to prepare_session for different Session classes
430
+
431
+ def session_class
432
+ SessionHash
433
+ end
434
+
435
+ # All thread safety and session retrieval procedures should occur here.
436
+ # Should return [session_id, session].
437
+ # If nil is provided as the session id, generation of a new valid id
438
+ # should occur within.
439
+
440
+ def find_session(env, sid)
441
+ raise '#find_session not implemented.'
442
+ end
443
+
444
+ # All thread safety and session storage procedures should occur here.
445
+ # Must return the session id if the session was saved successfully, or
446
+ # false if the session could not be saved.
447
+
448
+ def write_session(req, sid, session, options)
449
+ raise '#write_session not implemented.'
450
+ end
451
+
452
+ # All thread safety and session destroy procedures should occur here.
453
+ # Should return a new session id or nil if options[:drop]
454
+
455
+ def delete_session(req, sid, options)
456
+ raise '#delete_session not implemented'
457
+ end
458
+ end
459
+
460
+ class PersistedSecure < Persisted
461
+ class SecureSessionHash < SessionHash
462
+ def [](key)
463
+ if key == "session_id"
464
+ load_for_read!
465
+ case id
466
+ when SessionId
467
+ id.public_id
468
+ else
469
+ id
470
+ end
471
+ else
472
+ super
473
+ end
474
+ end
475
+ end
476
+
477
+ def generate_sid(*)
478
+ public_id = super
479
+
480
+ SessionId.new(public_id)
481
+ end
482
+
483
+ def extract_session_id(*)
484
+ public_id = super
485
+ public_id && SessionId.new(public_id)
486
+ end
487
+
488
+ private
489
+
490
+ def session_class
491
+ SecureSessionHash
492
+ end
493
+
494
+ def cookie_value(data)
495
+ data.cookie_value
496
+ end
497
+ end
498
+
499
+ class ID < Persisted
500
+ def self.inherited(klass)
501
+ k = klass.ancestors.find { |kl| kl.respond_to?(:superclass) && kl.superclass == ID }
502
+ unless k.instance_variable_defined?(:"@_rack_warned")
503
+ warn "#{klass} is inheriting from #{ID}. Inheriting from #{ID} is deprecated, please inherit from #{Persisted} instead" if $VERBOSE
504
+ k.instance_variable_set(:"@_rack_warned", true)
505
+ end
506
+ super
507
+ end
508
+
509
+ # All thread safety and session retrieval procedures should occur here.
510
+ # Should return [session_id, session].
511
+ # If nil is provided as the session id, generation of a new valid id
512
+ # should occur within.
513
+
514
+ def find_session(req, sid)
515
+ get_session req.env, sid
516
+ end
517
+
518
+ # All thread safety and session storage procedures should occur here.
519
+ # Must return the session id if the session was saved successfully, or
520
+ # false if the session could not be saved.
521
+
522
+ def write_session(req, sid, session, options)
523
+ set_session req.env, sid, session, options
524
+ end
525
+
526
+ # All thread safety and session destroy procedures should occur here.
527
+ # Should return a new session id or nil if options[:drop]
528
+
529
+ def delete_session(req, sid, options)
530
+ destroy_session req.env, sid, options
531
+ end
532
+ end
533
+ end
534
+ end
535
+ end
@@ -0,0 +1,13 @@
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
+ RACK_SESSION = 'rack.session'
10
+ RACK_SESSION_OPTIONS = 'rack.session.options'
11
+ RACK_SESSION_UNPACKED_COOKIE_DATA = 'rack.session.unpacked_cookie_data'
12
+ end
13
+ end
@@ -0,0 +1,313 @@
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
+ # Copyright, 2022, by Jon Dufresne.
7
+
8
+ require 'openssl'
9
+ require 'zlib'
10
+ require 'json'
11
+ require 'base64'
12
+ require 'delegate'
13
+
14
+ require 'rack/constants'
15
+ require 'rack/utils'
16
+
17
+ require_relative 'abstract/id'
18
+ require_relative 'encryptor'
19
+ require_relative 'constants'
20
+
21
+ module Rack
22
+
23
+ module Session
24
+
25
+ # Rack::Session::Cookie provides simple cookie based session management.
26
+ # By default, the session is a Ruby Hash that is serialized and encoded as
27
+ # a cookie set to :key (default: rack.session).
28
+ #
29
+ # This middleware accepts a :secrets option which enables encryption of
30
+ # session cookies. This option should be one or more random "secret keys"
31
+ # that are each at least 64 bytes in length. Multiple secret keys can be
32
+ # supplied in an Array, which is useful when rotating secrets.
33
+ #
34
+ # Several options are also accepted that are passed to Rack::Session::Encryptor.
35
+ # These options include:
36
+ # * :serialize_json
37
+ # Use JSON for message serialization instead of Marshal. This can be
38
+ # viewed as a security enhancement.
39
+ # * :gzip_over
40
+ # For message data over this many bytes, compress it with the deflate
41
+ # algorithm.
42
+ #
43
+ # Refer to Rack::Session::Encryptor for more details on these options.
44
+ #
45
+ # Prior to version TODO, the session hash was stored as base64 encoded
46
+ # marshalled data. When a :secret option was supplied, the integrity of the
47
+ # encoded data was protected with HMAC-SHA1. This functionality is still
48
+ # supported using a set of a legacy options.
49
+ #
50
+ # Lastly, a :coder option is also accepted. When used, both encryption and
51
+ # the legacy HMAC will be skipped. This option could create security issues
52
+ # in your application!
53
+ #
54
+ # Example:
55
+ #
56
+ # use Rack::Session::Cookie, {
57
+ # key: 'rack.session',
58
+ # domain: 'foo.com',
59
+ # path: '/',
60
+ # expire_after: 2592000,
61
+ # secrets: 'a randomly generated, raw binary string 64 bytes in size',
62
+ # }
63
+ #
64
+ # Example using legacy HMAC options:
65
+ #
66
+ # Rack::Session:Cookie.new(application, {
67
+ # # The secret used for legacy HMAC cookies, this enables the functionality
68
+ # legacy_hmac_secret: 'legacy secret',
69
+ # # legacy_hmac_coder will default to Rack::Session::Cookie::Base64::Marshal
70
+ # legacy_hmac_coder: Rack::Session::Cookie::Identity.new,
71
+ # # legacy_hmac will default to OpenSSL::Digest::SHA1
72
+ # legacy_hmac: OpenSSL::Digest::SHA256
73
+ # })
74
+ #
75
+ # Example of a cookie with no encoding:
76
+ #
77
+ # Rack::Session::Cookie.new(application, {
78
+ # :coder => Rack::Session::Cookie::Identity.new
79
+ # })
80
+ #
81
+ # Example of a cookie with custom encoding:
82
+ #
83
+ # Rack::Session::Cookie.new(application, {
84
+ # :coder => Class.new {
85
+ # def encode(str); str.reverse; end
86
+ # def decode(str); str.reverse; end
87
+ # }.new
88
+ # })
89
+ #
90
+
91
+ class Cookie < Abstract::PersistedSecure
92
+ # Encode session cookies as Base64
93
+ class Base64
94
+ def encode(str)
95
+ ::Base64.strict_encode64(str)
96
+ end
97
+
98
+ def decode(str)
99
+ ::Base64.decode64(str)
100
+ end
101
+
102
+ # Encode session cookies as Marshaled Base64 data
103
+ class Marshal < Base64
104
+ def encode(str)
105
+ super(::Marshal.dump(str))
106
+ end
107
+
108
+ def decode(str)
109
+ return unless str
110
+ ::Marshal.load(super(str)) rescue nil
111
+ end
112
+ end
113
+
114
+ # N.B. Unlike other encoding methods, the contained objects must be a
115
+ # valid JSON composite type, either a Hash or an Array.
116
+ class JSON < Base64
117
+ def encode(obj)
118
+ super(::JSON.dump(obj))
119
+ end
120
+
121
+ def decode(str)
122
+ return unless str
123
+ ::JSON.parse(super(str)) rescue nil
124
+ end
125
+ end
126
+
127
+ class ZipJSON < Base64
128
+ def encode(obj)
129
+ super(Zlib::Deflate.deflate(::JSON.dump(obj)))
130
+ end
131
+
132
+ def decode(str)
133
+ return unless str
134
+ ::JSON.parse(Zlib::Inflate.inflate(super(str)))
135
+ rescue
136
+ nil
137
+ end
138
+ end
139
+ end
140
+
141
+ # Use no encoding for session cookies
142
+ class Identity
143
+ def encode(str); str; end
144
+ def decode(str); str; end
145
+ end
146
+
147
+ class Marshal
148
+ def encode(str)
149
+ ::Marshal.dump(str)
150
+ end
151
+
152
+ def decode(str)
153
+ ::Marshal.load(str) if str
154
+ end
155
+ end
156
+
157
+ attr_reader :coder, :encryptors
158
+
159
+ def initialize(app, options = {})
160
+ # support both :secrets and :secret for backwards compatibility
161
+ secrets = [*(options[:secrets] || options[:secret])]
162
+
163
+ encryptor_opts = {
164
+ purpose: options[:key], serialize_json: options[:serialize_json]
165
+ }
166
+
167
+ # For each secret, create an Encryptor. We have iterate this Array at
168
+ # decryption time to achieve key rotation.
169
+ @encryptors = secrets.map do |secret|
170
+ Rack::Session::Encryptor.new secret, encryptor_opts
171
+ end
172
+
173
+ # If a legacy HMAC secret is present, initialize those features.
174
+ # Fallback to :secret for backwards compatibility.
175
+ if options.has_key?(:legacy_hmac_secret) || options.has_key?(:secret)
176
+ @legacy_hmac = options.fetch(:legacy_hmac, 'SHA1')
177
+
178
+ @legacy_hmac_secret = options[:legacy_hmac_secret] || options[:secret]
179
+ @legacy_hmac_coder = options.fetch(:legacy_hmac_coder, Base64::Marshal.new)
180
+ else
181
+ @legacy_hmac = false
182
+ end
183
+
184
+ warn <<-MSG unless secure?(options)
185
+ SECURITY WARNING: No secret option provided to Rack::Session::Cookie.
186
+ This poses a security threat. It is strongly recommended that you
187
+ provide a secret to prevent exploits that may be possible from crafted
188
+ cookies. This will not be supported in future versions of Rack, and
189
+ future versions will even invalidate your existing user cookies.
190
+
191
+ Called from: #{caller[0]}.
192
+ MSG
193
+
194
+ # Potential danger ahead! Marshal without verification and/or
195
+ # encryption could present a major security issue.
196
+ @coder = options[:coder] ||= Base64::Marshal.new
197
+
198
+ super(app, options.merge!(cookie_only: true))
199
+ end
200
+
201
+ private
202
+
203
+ def find_session(req, sid)
204
+ data = unpacked_cookie_data(req)
205
+ data = persistent_session_id!(data)
206
+ [data["session_id"], data]
207
+ end
208
+
209
+ def extract_session_id(request)
210
+ unpacked_cookie_data(request)&.[]("session_id")
211
+ end
212
+
213
+ def unpacked_cookie_data(request)
214
+ request.fetch_header(RACK_SESSION_UNPACKED_COOKIE_DATA) do |k|
215
+ if cookie_data = request.cookies[@key]
216
+ session_data = nil
217
+
218
+ # Try to decrypt the session data with our encryptors
219
+ encryptors.each do |encryptor|
220
+ begin
221
+ session_data = encryptor.decrypt(cookie_data)
222
+ break
223
+ rescue Rack::Session::Encryptor::Error => error
224
+ request.env[Rack::RACK_ERRORS].puts "Session cookie encryptor error: #{error.message}"
225
+
226
+ next
227
+ end
228
+ end
229
+
230
+ # If session decryption fails but there is @legacy_hmac_secret
231
+ # defined, attempt legacy HMAC verification
232
+ if !session_data && @legacy_hmac_secret
233
+ # Parse and verify legacy HMAC session cookie
234
+ session_data, _, digest = cookie_data.rpartition('--')
235
+ session_data = nil unless legacy_digest_match?(session_data, digest)
236
+
237
+ # Decode using legacy HMAC decoder
238
+ session_data = @legacy_hmac_coder.decode(session_data)
239
+
240
+ elsif !session_data && coder
241
+ # Use the coder option, which has the potential to be very unsafe
242
+ session_data = coder.decode(cookie_data)
243
+ end
244
+ end
245
+
246
+ request.set_header(k, session_data || {})
247
+ end
248
+ end
249
+
250
+ def persistent_session_id!(data, sid = nil)
251
+ data ||= {}
252
+ data["session_id"] ||= sid || generate_sid
253
+ data
254
+ end
255
+
256
+ class SessionId < DelegateClass(Session::SessionId)
257
+ attr_reader :cookie_value
258
+
259
+ def initialize(session_id, cookie_value)
260
+ super(session_id)
261
+ @cookie_value = cookie_value
262
+ end
263
+ end
264
+
265
+ def write_session(req, session_id, session, options)
266
+ session = session.merge("session_id" => session_id)
267
+ session_data = encode_session_data(session)
268
+
269
+ if session_data.size > (4096 - @key.size)
270
+ req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
271
+ nil
272
+ else
273
+ SessionId.new(session_id, session_data)
274
+ end
275
+ end
276
+
277
+ def delete_session(req, session_id, options)
278
+ # Nothing to do here, data is in the client
279
+ generate_sid unless options[:drop]
280
+ end
281
+
282
+ def legacy_digest_match?(data, digest)
283
+ return false unless data && digest
284
+
285
+ Rack::Utils.secure_compare(digest, legacy_generate_hmac(data))
286
+ end
287
+
288
+ def legacy_generate_hmac(data)
289
+ OpenSSL::HMAC.hexdigest(@legacy_hmac, @legacy_hmac_secret, data)
290
+ end
291
+
292
+ def encode_session_data(session)
293
+ if encryptors.empty?
294
+ coder.encode(session)
295
+ else
296
+ encryptors.first.encrypt(session)
297
+ end
298
+ end
299
+
300
+ # Were consider "secure" if:
301
+ # * Encrypted cookies are enabled and one or more encryptor is
302
+ # initialized
303
+ # * The legacy HMAC option is enabled
304
+ # * Customer :coder is used, with :let_coder_handle_secure_encoding
305
+ # set to true
306
+ def secure?(options)
307
+ !@encryptors.empty? ||
308
+ @legacy_hmac ||
309
+ (options[:coder] && options[:let_coder_handle_secure_encoding])
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,192 @@
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 'openssl'
9
+ require 'securerandom'
10
+ require 'zlib'
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
+ # The secret String must be at least 64 bytes in size. The first 32 bytes
27
+ # will be used for the encryption cipher key. The remainder will be used
28
+ # for an HMAC key.
29
+ #
30
+ # Options may include:
31
+ # * :serialize_json
32
+ # Use JSON for message serialization instead of Marshal. This can be
33
+ # viewed as a security enhancement.
34
+ # * :pad_size
35
+ # Pad encrypted message data, to a multiple of this many bytes
36
+ # (default: 32). This can be between 2-4096 bytes, or +nil+ to disable
37
+ # padding.
38
+ # * :purpose
39
+ # Limit messages to a specific purpose. This can be viewed as a
40
+ # security enhancement to prevent message reuse from different contexts
41
+ # if keys are reused.
42
+ #
43
+ # Cryptography and Output Format:
44
+ #
45
+ # urlsafe_encode64(version + random_data + IV + encrypted data + HMAC)
46
+ #
47
+ # Where:
48
+ # * version - 1 byte and is currently always 0x01
49
+ # * random_data - 32 bytes used for generating the per-message secret
50
+ # * IV - 16 bytes random initialization vector
51
+ # * HMAC - 32 bytes HMAC-SHA-256 of all preceding data, plus the purpose
52
+ # value
53
+ def initialize(secret, opts = {})
54
+ raise ArgumentError, "secret must be a String" unless String === secret
55
+ raise ArgumentError, "invalid secret: #{secret.bytesize}, must be >=64" unless secret.bytesize >= 64
56
+
57
+ case opts[:pad_size]
58
+ when nil
59
+ # padding is disabled
60
+ when Integer
61
+ raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}" unless (2..4096).include? opts[:pad_size]
62
+ else
63
+ raise ArgumentError, "invalid pad_size: #{opts[:pad_size]}; must be Integer or nil"
64
+ end
65
+
66
+ @options = {
67
+ serialize_json: false, pad_size: 32, purpose: nil
68
+ }.update(opts)
69
+
70
+ @hmac_secret = secret.dup.force_encoding('BINARY')
71
+ @cipher_secret = @hmac_secret.slice!(0, 32)
72
+
73
+ @hmac_secret.freeze
74
+ @cipher_secret.freeze
75
+ end
76
+
77
+ def decrypt(base64_data)
78
+ data = Base64.urlsafe_decode64(base64_data)
79
+
80
+ signature = data.slice!(-32..-1)
81
+
82
+ verify_authenticity! data, signature
83
+
84
+ # The version is reserved for future
85
+ _version = data.slice!(0, 1)
86
+ message_secret = data.slice!(0, 32)
87
+ cipher_iv = data.slice!(0, 16)
88
+
89
+ cipher = new_cipher
90
+ cipher.decrypt
91
+
92
+ set_cipher_key(cipher, cipher_secret_from_message_secret(message_secret))
93
+
94
+ cipher.iv = cipher_iv
95
+ data = cipher.update(data) << cipher.final
96
+
97
+ deserialized_message data
98
+ rescue ArgumentError
99
+ raise InvalidSignature, 'Message invalid'
100
+ end
101
+
102
+ def encrypt(message)
103
+ version = "\1"
104
+
105
+ serialized_payload = serialize_payload(message)
106
+ message_secret, cipher_secret = new_message_and_cipher_secret
107
+
108
+ cipher = new_cipher
109
+ cipher.encrypt
110
+
111
+ set_cipher_key(cipher, cipher_secret)
112
+
113
+ cipher_iv = cipher.random_iv
114
+
115
+ encrypted_data = cipher.update(serialized_payload) << cipher.final
116
+
117
+ data = String.new
118
+ data << version
119
+ data << message_secret
120
+ data << cipher_iv
121
+ data << encrypted_data
122
+ data << compute_signature(data)
123
+
124
+ Base64.urlsafe_encode64(data)
125
+ end
126
+
127
+ private
128
+
129
+ def new_cipher
130
+ OpenSSL::Cipher.new('aes-256-ctr')
131
+ end
132
+
133
+ def new_message_and_cipher_secret
134
+ message_secret = SecureRandom.random_bytes(32)
135
+
136
+ [message_secret, cipher_secret_from_message_secret(message_secret)]
137
+ end
138
+
139
+ def cipher_secret_from_message_secret(message_secret)
140
+ OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @cipher_secret, message_secret)
141
+ end
142
+
143
+ def set_cipher_key(cipher, key)
144
+ cipher.key = key
145
+ end
146
+
147
+ def serializer
148
+ @serializer ||= @options[:serialize_json] ? JSON : Marshal
149
+ end
150
+
151
+ def compute_signature(data)
152
+ signing_data = data
153
+ signing_data += @options[:purpose] if @options[:purpose]
154
+
155
+ OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @hmac_secret, signing_data)
156
+ end
157
+
158
+ def verify_authenticity!(data, signature)
159
+ raise InvalidMessage, 'Message is invalid' if data.nil? || signature.nil?
160
+
161
+ unless Rack::Utils.secure_compare(signature, compute_signature(data))
162
+ raise InvalidSignature, 'HMAC is invalid'
163
+ end
164
+ end
165
+
166
+ # Returns a serialized payload of the message. If a :pad_size is supplied,
167
+ # the message will be padded. The first 2 bytes of the returned string will
168
+ # indicating the amount of padding.
169
+ def serialize_payload(message)
170
+ serialized_data = serializer.dump(message)
171
+
172
+ return "#{[0].pack('v')}#{serialized_data}" if @options[:pad_size].nil?
173
+
174
+ padding_bytes = @options[:pad_size] - (2 + serialized_data.size) % @options[:pad_size]
175
+ padding_data = SecureRandom.random_bytes(padding_bytes)
176
+
177
+ "#{[padding_bytes].pack('v')}#{padding_data}#{serialized_data}"
178
+ end
179
+
180
+ # Return the deserialized message. The first 2 bytes will be read as the
181
+ # amount of padding.
182
+ def deserialized_message(data)
183
+ # Read the first 2 bytes as the padding_bytes size
184
+ padding_bytes, = data.unpack('v')
185
+
186
+ # Slice out the serialized_data and deserialize it
187
+ serialized_data = data.slice(2 + padding_bytes, data.bytesize)
188
+ serializer.load serialized_data
189
+ end
190
+ end
191
+ end
192
+ 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
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Rack
7
7
  module Session
8
- VERSION = "1.0.2"
8
+ VERSION = "2.1.1"
9
9
  end
10
10
  end
data/lib/rack/session.rb CHANGED
@@ -1,5 +1,13 @@
1
- # Intentional NO-OP require target!
2
- #
3
- # See:
4
- # - https://github.com/rack/rack-session/issues/15
5
- # - https://github.com/rack/rack-session/issues/26
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
+ autoload :Memcache, "rack/session/memcache"
12
+ end
13
+ end
data/license.md CHANGED
@@ -1,7 +1,55 @@
1
1
  # MIT License
2
2
 
3
- Copyright, 2022-2023, by Samuel Williams.
4
- Copyright, 2022, by Jeremy Evans.
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,27 @@
1
+ # Releases
2
+
3
+ ## v2.1.1
4
+
5
+ - Prevent `Rack::Session::Pool` from recreating deleted sessions [CVE-2025-46336](https://github.com/rack/rack-session/security/advisories/GHSA-9j94-67jr-4cqj).
6
+
7
+ ## v2.1.0
8
+
9
+ - Improved compatibility with Ruby 3.3+ and Rack 3+.
10
+ - Add support for cookie option `partitioned`.
11
+ - Introduce `assume_ssl` option to allow secure session cookies through insecure proxy.
12
+
13
+ ## v2.0.0
14
+
15
+ - Initial migration of code from Rack 2, for Rack 3 release.
16
+
17
+ ## v1.0.2
18
+
19
+ - Fix missing `rack/session.rb` file.
20
+
21
+ ## v1.0.1
22
+
23
+ - Pin to `rack < 3`.
24
+
25
+ ## v1.0.0
26
+
27
+ - Empty shim release for Rack 2.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-session
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.2
4
+ version: 2.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -11,22 +11,36 @@ authors:
11
11
  autorequire:
12
12
  bindir: bin
13
13
  cert_chain: []
14
- date: 2023-12-02 00:00:00.000000000 Z
14
+ date: 2025-05-06 00:00:00.000000000 Z
15
15
  dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: base64
18
+ requirement: !ruby/object:Gem::Requirement
19
+ requirements:
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 0.1.0
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ version: 0.1.0
16
30
  - !ruby/object:Gem::Dependency
17
31
  name: rack
18
32
  requirement: !ruby/object:Gem::Requirement
19
33
  requirements:
20
- - - "<"
34
+ - - ">="
21
35
  - !ruby/object:Gem::Version
22
- version: '3'
36
+ version: 3.0.0
23
37
  type: :runtime
24
38
  prerelease: false
25
39
  version_requirements: !ruby/object:Gem::Requirement
26
40
  requirements:
27
- - - "<"
41
+ - - ">="
28
42
  - !ruby/object:Gem::Version
29
- version: '3'
43
+ version: 3.0.0
30
44
  - !ruby/object:Gem::Dependency
31
45
  name: bundler
32
46
  requirement: !ruby/object:Gem::Requirement
@@ -104,14 +118,21 @@ extensions: []
104
118
  extra_rdoc_files: []
105
119
  files:
106
120
  - lib/rack/session.rb
121
+ - lib/rack/session/abstract/id.rb
122
+ - lib/rack/session/constants.rb
123
+ - lib/rack/session/cookie.rb
124
+ - lib/rack/session/encryptor.rb
125
+ - lib/rack/session/pool.rb
107
126
  - lib/rack/session/version.rb
108
127
  - license.md
109
128
  - readme.md
129
+ - releases.md
110
130
  - security.md
111
131
  homepage: https://github.com/rack/rack-session
112
132
  licenses:
113
133
  - MIT
114
- metadata: {}
134
+ metadata:
135
+ rubygems_mfa_required: 'true'
115
136
  post_install_message:
116
137
  rdoc_options: []
117
138
  require_paths:
@@ -120,14 +141,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
120
141
  requirements:
121
142
  - - ">="
122
143
  - !ruby/object:Gem::Version
123
- version: 2.4.0
144
+ version: '2.5'
124
145
  required_rubygems_version: !ruby/object:Gem::Requirement
125
146
  requirements:
126
147
  - - ">="
127
148
  - !ruby/object:Gem::Version
128
149
  version: '0'
129
150
  requirements: []
130
- rubygems_version: 3.4.22
151
+ rubygems_version: 3.5.22
131
152
  signing_key:
132
153
  specification_version: 4
133
154
  summary: A session implementation for Rack.