3 require 'base64' # to convert Marshal.dump to ASCII
4 require 'openssl' # to generate the HMAC message digest
6 # This cookie-based session store is the Rails default. Sessions typically
7 # contain at most a user_id and flash message; both fit within the 4K cookie
8 # size limit. Cookie-based sessions are dramatically faster than the
11 # If you have more than 4K of session data or don't want your data to be
12 # visible to the user, pick another session store.
14 # CookieOverflow is raised if you attempt to store more than 4K of data.
15 # TamperedWithCookie is raised if the data integrity check fails.
17 # A message digest is included with the cookie to ensure data integrity:
18 # a user cannot alter his user_id without knowing the secret key included in
19 # the hash. New apps are generated with a pregenerated secret in
20 # config/environment.rb. Set your own for old apps you're upgrading.
23 # :secret An application-wide key string or block returning a string
24 # called per generated digest. The block is called with the
25 # CGI::Session instance as an argument. It's important that the
26 # secret is not vulnerable to a dictionary attack. Therefore,
27 # you should choose a secret consisting of random numbers and
28 # letters and preferably more than 30 characters.
30 # Example: :secret => '449fe2e7daee471bffae2fd8dc02313d'
31 # :secret => Proc.new { User.current_user.secret_key }
33 # :digest The message digest algorithm used to verify session integrity
34 # defaults to 'SHA1' but may be any digest provided by OpenSSL,
35 # such as 'MD5', 'RIPEMD160', 'SHA256', etc.
37 # Note that changing digest or secret invalidates all existing sessions!
38 class CGI::Session::CookieStore
39 # Cookies can typically store 4096 bytes.
42 # Raised when storing more than 4K of session data.
43 class CookieOverflow < StandardError; end
45 # Raised when the cookie fails its integrity check.
46 class TamperedWithCookie < StandardError; end
48 # Called from CGI::Session only.
49 def initialize(session, options = {})
50 # The session_key option is required.
51 if options['session_key'].blank?
52 raise ArgumentError, 'A session_key is required to write a cookie containing the session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase" } in config/environment.rb'
55 # The secret option is required.
56 ensure_secret_secure(options['secret'])
58 # Keep the session and its secret on hand so we can read and write cookies.
59 @session, @secret = session, options['secret']
61 # Message digest defaults to SHA1.
62 @digest = options['digest'] || 'SHA1'
64 # Default cookie options derived from session settings.
66 'name' => options['session_key'],
67 'path' => options['session_path'],
68 'domain' => options['session_domain'],
69 'expires' => options['session_expires'],
70 'secure' => options['session_secure']
73 # Set no_hidden and no_cookies since the session id is unused and we
74 # set our own data cookie.
75 options['no_hidden'] = true
76 options['no_cookies'] = true
79 # To prevent users from using something insecure like "Password" we make sure that the
80 # secret they've provided is at least 30 characters in length.
81 def ensure_secret_secure(secret)
82 # There's no way we can do this check if they've provided a proc for the
84 return true if secret.is_a?(Proc)
87 raise ArgumentError, 'A secret is required to generate an integrity hash for cookie session data. Use config.action_controller.session = { :session_key => "_myapp_session", :secret => "some secret phrase" } in config/environment.rb'
91 raise ArgumentError, "Secret should be something secure, like #{CGI::Session.generate_unique_id}. The value you provided: [#{secret}]"
95 # Restore session data from the cookie.
97 @original = read_cookie
98 @data = unmarshal(@original) || {}
101 # Wait until close to write the session data cookie.
104 # Write the session data cookie if it was loaded and has changed.
106 if defined?(@data) && !@data.blank?
107 updated = marshal(@data)
108 raise CookieOverflow if updated.size > MAX
109 write_cookie('value' => updated) unless updated == @original
113 # Delete the session data by setting an expired cookie with no data.
116 clear_old_cookie_value
117 write_cookie('value' => '', 'expires' => 1.year.ago)
120 # Generate the HMAC keyed message digest. Uses SHA1 by default.
121 def generate_digest(data)
122 key = @secret.respond_to?(:call) ? @secret.call(@session) : @secret
123 OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), key, data)
127 # Marshal a session hash into safe cookie data. Include an integrity hash.
129 data = Base64.encode64(Marshal.dump(session)).chop
130 CGI.escape "#{data}--#{generate_digest(data)}"
133 # Unmarshal cookie data to a hash and verify its integrity.
134 def unmarshal(cookie)
136 data, digest = CGI.unescape(cookie).split('--')
137 unless digest == generate_digest(data)
139 raise TamperedWithCookie
141 Marshal.load(Base64.decode64(data))
145 # Read the session data cookie.
147 @session.cgi.cookies[@cookie_options['name']].first
150 # CGI likes to make you hack.
151 def write_cookie(options)
152 cookie = CGI::Cookie.new(@cookie_options.merge(options))
153 @session.cgi.send :instance_variable_set, '@output_cookies', [cookie]
156 # Clear cookie value so subsequent new_session doesn't reload old data.
157 def clear_old_cookie_value
158 @session.cgi.cookies[@cookie_options['name']].clear