class Rack::Session::Cookie
def delete_session(req, session_id, options)
def delete_session(req, session_id, options) # Nothing to do here, data is in the client generate_sid unless options[:drop] end
def encode_session_data(session)
def encode_session_data(session) if encryptors.empty? coder.encode(session) else encryptors.first.encrypt(session) end end
def extract_session_id(request)
def extract_session_id(request) unpacked_cookie_data(request)["session_id"] end
def find_session(req, sid)
def find_session(req, sid) data = unpacked_cookie_data(req) data = persistent_session_id!(data) [data["session_id"], data] end
def initialize(app, options = {})
def initialize(app, options = {}) # support both :secrets and :secret for backwards compatibility secrets = [*(options[:secrets] || options[:secret])] encryptor_opts = { purpose: options[:key], serialize_json: options[:serialize_json] } # For each secret, create an Encryptor. We have iterate this Array at # decryption time to achieve key rotation. @encryptors = secrets.map do |secret| Rack::Session::Encryptor.new secret, encryptor_opts end # If a legacy HMAC secret is present, initialize those features. # Fallback to :secret for backwards compatibility. if options.has_key?(:legacy_hmac_secret) || options.has_key?(:secret) @legacy_hmac = options.fetch(:legacy_hmac, 'SHA1') @legacy_hmac_secret = options[:legacy_hmac_secret] || options[:secret] @legacy_hmac_coder = options.fetch(:legacy_hmac_coder, Base64::Marshal.new) else @legacy_hmac = false end warn <<-MSG unless secure?(options) SECURITY WARNING: No secret option provided to Rack::Session::Cookie. This poses a security threat. It is strongly recommended that you provide a secret to prevent exploits that may be possible from crafted cookies. This will not be supported in future versions of Rack, and future versions will even invalidate your existing user cookies. Called from: #{caller[0]}. MSG # Potential danger ahead! Marshal without verification and/or # encryption could present a major security issue. @coder = options[:coder] ||= Base64::Marshal.new super(app, options.merge!(cookie_only: true)) end
def legacy_digest_match?(data, digest)
def legacy_digest_match?(data, digest) return false unless data && digest Rack::Utils.secure_compare(digest, legacy_generate_hmac(data)) end
def legacy_generate_hmac(data)
def legacy_generate_hmac(data) OpenSSL::HMAC.hexdigest(@legacy_hmac, @legacy_hmac_secret, data) end
def persistent_session_id!(data, sid = nil)
def persistent_session_id!(data, sid = nil) data ||= {} data["session_id"] ||= sid || generate_sid data end
def secure?(options)
* Customer :coder is used, with :let_coder_handle_secure_encoding
* The legacy HMAC option is enabled
initialized
* Encrypted cookies are enabled and one or more encryptor is
Were consider "secure" if:
def secure?(options) !@encryptors.empty? || @legacy_hmac || (options[:coder] && options[:let_coder_handle_secure_encoding]) end
def unpacked_cookie_data(request)
def unpacked_cookie_data(request) request.fetch_header(RACK_SESSION_UNPACKED_COOKIE_DATA) do |k| cookie_data = request.cookies[@key] session_data = nil # Try to decrypt the session data with our encryptors encryptors.each do |encryptor| begin session_data = encryptor.decrypt(cookie_data) if cookie_data break rescue Rack::Session::Encryptor::Error => error request.env[Rack::RACK_ERRORS].puts "Session cookie encryptor error: #{error.message}" next end end # If session decryption fails but there is @legacy_hmac_secret # defined, attempt legacy HMAC verification if !session_data && @legacy_hmac_secret # Parse and verify legacy HMAC session cookie session_data, _, digest = cookie_data.rpartition('--') session_data = nil unless legacy_digest_match?(session_data, digest) # Decode using legacy HMAC decoder session_data = @legacy_hmac_coder.decode(session_data) elsif !session_data && coder # Use the coder option, which has the potential to be very unsafe session_data = coder.decode(cookie_data) end request.set_header(k, session_data || {}) end end
def write_session(req, session_id, session, options)
def write_session(req, session_id, session, options) session = session.merge("session_id" => session_id) session_data = encode_session_data(session) if session_data.size > (4096 - @key.size) req.get_header(RACK_ERRORS).puts("Warning! Rack::Session::Cookie data size exceeds 4K.") nil else SessionId.new(session_id, session_data) end end