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,315 @@
|
|
|
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 && encryptors.empty? && coder
|
|
241
|
+
# Use the coder option, which has the potential to be very unsafe.
|
|
242
|
+
# This path is only reached when no encryptors (secrets:) are configured;
|
|
243
|
+
# if encryptors are present but decryption failed, the cookie is rejected.
|
|
244
|
+
session_data = coder.decode(cookie_data)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
request.set_header(k, session_data || {})
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def persistent_session_id!(data, sid = nil)
|
|
253
|
+
data ||= {}
|
|
254
|
+
data["session_id"] ||= sid || generate_sid
|
|
255
|
+
data
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
class SessionId < DelegateClass(Session::SessionId)
|
|
259
|
+
attr_reader :cookie_value
|
|
260
|
+
|
|
261
|
+
def initialize(session_id, cookie_value)
|
|
262
|
+
super(session_id)
|
|
263
|
+
@cookie_value = cookie_value
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def write_session(req, session_id, session, options)
|
|
268
|
+
session = session.merge("session_id" => session_id)
|
|
269
|
+
session_data = encode_session_data(session)
|
|
270
|
+
|
|
271
|
+
if session_data.size > (4096 - @key.size)
|
|
272
|
+
req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.")
|
|
273
|
+
nil
|
|
274
|
+
else
|
|
275
|
+
SessionId.new(session_id, session_data)
|
|
276
|
+
end
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def delete_session(req, session_id, options)
|
|
280
|
+
# Nothing to do here, data is in the client
|
|
281
|
+
generate_sid unless options[:drop]
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def legacy_digest_match?(data, digest)
|
|
285
|
+
return false unless data && digest
|
|
286
|
+
|
|
287
|
+
Rack::Utils.secure_compare(digest, legacy_generate_hmac(data))
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def legacy_generate_hmac(data)
|
|
291
|
+
OpenSSL::HMAC.hexdigest(@legacy_hmac, @legacy_hmac_secret, data)
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
def encode_session_data(session)
|
|
295
|
+
if encryptors.empty?
|
|
296
|
+
coder.encode(session)
|
|
297
|
+
else
|
|
298
|
+
encryptors.first.encrypt(session)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# Were consider "secure" if:
|
|
303
|
+
# * Encrypted cookies are enabled and one or more encryptor is
|
|
304
|
+
# initialized
|
|
305
|
+
# * The legacy HMAC option is enabled
|
|
306
|
+
# * Customer :coder is used, with :let_coder_handle_secure_encoding
|
|
307
|
+
# set to true
|
|
308
|
+
def secure?(options)
|
|
309
|
+
!@encryptors.empty? ||
|
|
310
|
+
@legacy_hmac ||
|
|
311
|
+
(options[:coder] && options[:let_coder_handle_secure_encoding])
|
|
312
|
+
end
|
|
313
|
+
end
|
|
314
|
+
end
|
|
315
|
+
end
|