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.
@@ -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