Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/actionpack/lib/action_controller/session/cookie_store.rb

Revision 8589, 6.2 kB (checked in by bitsweat, 2 years ago)

Ruby 1.9 compat: cookie store delete sets nil value instead of empty string

Line 
1 require 'cgi'
2 require 'cgi/session'
3 require 'openssl'       # to generate the HMAC message digest
4
5 # This cookie-based session store is the Rails default. Sessions typically
6 # contain at most a user_id and flash message; both fit within the 4K cookie
7 # size limit. Cookie-based sessions are dramatically faster than the
8 # alternatives.
9 #
10 # If you have more than 4K of session data or don't want your data to be
11 # visible to the user, pick another session store.
12 #
13 # CookieOverflow is raised if you attempt to store more than 4K of data.
14 # TamperedWithCookie is raised if the data integrity check fails.
15 #
16 # A message digest is included with the cookie to ensure data integrity:
17 # a user cannot alter his user_id without knowing the secret key included in
18 # the hash. New apps are generated with a pregenerated secret in
19 # config/environment.rb. Set your own for old apps you're upgrading.
20 #
21 # Session options:
22 #   :secret   An application-wide key string or block returning a string
23 #             called per generated digest. The block is called with the
24 #             CGI::Session instance as an argument. It's important that the
25 #             secret is not vulnerable to a dictionary attack. Therefore,
26 #             you should choose a secret consisting of random numbers and
27 #             letters and more than 30 characters.
28 #
29 #             Example:  :secret => '449fe2e7daee471bffae2fd8dc02313d'
30 #                       :secret => Proc.new { User.current_user.secret_key }
31 #
32 #   :digest   The message digest algorithm used to verify session integrity
33 #             defaults to 'SHA1' but may be any digest provided by OpenSSL,
34 #             such as 'MD5', 'RIPEMD160', 'SHA256', etc.
35 #
36 # To generate a secret key for an existing application, run
37 # `rake secret` and set the key in config/environment.rb
38 #
39 # Note that changing digest or secret invalidates all existing sessions!
40 class CGI::Session::CookieStore
41   # Cookies can typically store 4096 bytes.
42   MAX = 4096
43   SECRET_MIN_LENGTH = 30 # characters
44
45   # Raised when storing more than 4K of session data.
46   class CookieOverflow < StandardError; end
47
48   # Raised when the cookie fails its integrity check.
49   class TamperedWithCookie < StandardError; end
50
51   # Called from CGI::Session only.
52   def initialize(session, options = {})
53     # The session_key option is required.
54     if options['session_key'].blank?
55       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'
56     end
57
58     # The secret option is required.
59     ensure_secret_secure(options['secret'])
60
61     # Keep the session and its secret on hand so we can read and write cookies.
62     @session, @secret = session, options['secret']
63
64     # Message digest defaults to SHA1.
65     @digest = options['digest'] || 'SHA1'
66
67     # Default cookie options derived from session settings.
68     @cookie_options = {
69       'name'    => options['session_key'],
70       'path'    => options['session_path'],
71       'domain'  => options['session_domain'],
72       'expires' => options['session_expires'],
73       'secure'  => options['session_secure']
74     }
75
76     # Set no_hidden and no_cookies since the session id is unused and we
77     # set our own data cookie.
78     options['no_hidden'] = true
79     options['no_cookies'] = true
80   end
81
82   # To prevent users from using something insecure like "Password" we make sure that the
83   # secret they've provided is at least 30 characters in length.
84   def ensure_secret_secure(secret)
85     # There's no way we can do this check if they've provided a proc for the
86     # secret.
87     return true if secret.is_a?(Proc)
88
89     if secret.blank?
90       raise ArgumentError, %Q{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 of at least #{SECRET_MIN_LENGTH} characters" } in config/environment.rb}
91     end
92
93     if secret.length < SECRET_MIN_LENGTH
94       raise ArgumentError, %Q{Secret should be something secure, like "#{CGI::Session.generate_unique_id}".  The value you provided, "#{secret}", is shorter than the minimum length of #{SECRET_MIN_LENGTH} characters}
95     end
96   end
97
98   # Restore session data from the cookie.
99   def restore
100     @original = read_cookie
101     @data = unmarshal(@original) || {}
102   end
103
104   # Wait until close to write the session data cookie.
105   def update; end
106
107   # Write the session data cookie if it was loaded and has changed.
108   def close
109     if defined?(@data) && !@data.blank?
110       updated = marshal(@data)
111       raise CookieOverflow if updated.size > MAX
112       write_cookie('value' => updated) unless updated == @original
113     end
114   end
115
116   # Delete the session data by setting an expired cookie with no data.
117   def delete
118     @data = nil
119     clear_old_cookie_value
120     write_cookie('value' => nil, 'expires' => 1.year.ago)
121   end
122
123   # Generate the HMAC keyed message digest. Uses SHA1 by default.
124   def generate_digest(data)
125     key = @secret.respond_to?(:call) ? @secret.call(@session) : @secret
126     OpenSSL::HMAC.hexdigest(OpenSSL::Digest::Digest.new(@digest), key, data)
127   end
128
129   private
130     # Marshal a session hash into safe cookie data. Include an integrity hash.
131     def marshal(session)
132       data = ActiveSupport::Base64.encode64(Marshal.dump(session)).chop
133       CGI.escape "#{data}--#{generate_digest(data)}"
134     end
135
136     # Unmarshal cookie data to a hash and verify its integrity.
137     def unmarshal(cookie)
138       if cookie
139         data, digest = CGI.unescape(cookie).split('--')
140         unless digest == generate_digest(data)
141           delete
142           raise TamperedWithCookie
143         end
144         Marshal.load(ActiveSupport::Base64.decode64(data))
145       end
146     end
147
148     # Read the session data cookie.
149     def read_cookie
150       @session.cgi.cookies[@cookie_options['name']].first
151     end
152
153     # CGI likes to make you hack.
154     def write_cookie(options)
155       cookie = CGI::Cookie.new(@cookie_options.merge(options))
156       @session.cgi.send :instance_variable_set, '@output_cookies', [cookie]
157     end
158
159     # Clear cookie value so subsequent new_session doesn't reload old data.
160     def clear_old_cookie_value
161       @session.cgi.cookies[@cookie_options['name']].clear
162     end
163 end
Note: See TracBrowser for help on using the browser.