aboutsummaryrefslogblamecommitdiffstats
path: root/actionpack/lib/action_controller/session/cookie_store.rb
blob: ea0ea4f841faec6a02bacfe25401f3353b896959 (plain) (tree)
1
2
3
4
5
6
7
8

                     





                                                                            




                                                                        

                                                                        
                                                                              



                                                                    
 




                                                                                
 





                                                                           
 
                                                           
                                                         
 
                                                                        


                                           
                                     








                                                     




                                                                                                                                                                                                                                           
                                    
                                           



                                                                               


                                         





                                                           

                                                 







                                                                        







                                                                                        
                                                                                                                                                                                                                                                                                            

       

                                                                                                                                                                                                                        


       










                                                                   
                                       







                                                                      
               
                          
                                                         




                                                                              
                                




                                                               
                               
         


                                                           

       









                                                                           




                                                                           








                                                      
   
require 'cgi'
require 'cgi/session'

# This cookie-based session store is the Rails default. Sessions typically
# contain at most a user_id and flash message; both fit within the 4K cookie
# size limit. Cookie-based sessions are dramatically faster than the
# alternatives.
#
# If you have more than 4K of session data or don't want your data to be
# visible to the user, pick another session store.
#
# CookieOverflow is raised if you attempt to store more than 4K of data.
# TamperedWithCookie is raised if the data integrity check fails.
#
# A message digest is included with the cookie to ensure data integrity:
# a user cannot alter his +user_id+ without knowing the secret key included in
# the hash. New apps are generated with a pregenerated secret in
# config/environment.rb. Set your own for old apps you're upgrading.
#
# Session options:
#
# * <tt>:secret</tt>: An application-wide key string or block returning a string
#   called per generated digest. The block is called with the CGI::Session
#   instance as an argument. It's important that the secret is not vulnerable to
#   a dictionary attack. Therefore, you should choose a secret consisting of
#   random numbers and letters and more than 30 characters. Examples:
#
#     :secret => '449fe2e7daee471bffae2fd8dc02313d'
#     :secret => Proc.new { User.current_user.secret_key }
#
# * <tt>:digest</tt>: The message digest algorithm used to verify session
#   integrity defaults to 'SHA1' but may be any digest provided by OpenSSL,
#   such as 'MD5', 'RIPEMD160', 'SHA256', etc.
#
# To generate a secret key for an existing application, run
# "rake secret" and set the key in config/environment.rb.
#
# Note that changing digest or secret invalidates all existing sessions!
class CGI::Session::CookieStore
  # Cookies can typically store 4096 bytes.
  MAX = 4096
  SECRET_MIN_LENGTH = 30 # characters

  # Raised when storing more than 4K of session data.
  class CookieOverflow < StandardError; end

  # Raised when the cookie fails its integrity check.
  class TamperedWithCookie < StandardError; end

  # Called from CGI::Session only.
  def initialize(session, options = {})
    # The session_key option is required.
    if options['session_key'].blank?
      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'
    end

    # The secret option is required.
    ensure_secret_secure(options['secret'])

    # Keep the session and its secret on hand so we can read and write cookies.
    @session, @secret = session, options['secret']

    # Message digest defaults to SHA1.
    @digest = options['digest'] || 'SHA1'

    # Default cookie options derived from session settings.
    @cookie_options = {
      'name'    => options['session_key'],
      'path'    => options['session_path'],
      'domain'  => options['session_domain'],
      'expires' => options['session_expires'],
      'secure'  => options['session_secure'],
      'http_only' => options['session_http_only']
    }

    # Set no_hidden and no_cookies since the session id is unused and we
    # set our own data cookie.
    options['no_hidden'] = true
    options['no_cookies'] = true
  end

  # To prevent users from using something insecure like "Password" we make sure that the
  # secret they've provided is at least 30 characters in length.
  def ensure_secret_secure(secret)
    # There's no way we can do this check if they've provided a proc for the
    # secret.
    return true if secret.is_a?(Proc)

    if secret.blank?
      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}
    end

    if secret.length < SECRET_MIN_LENGTH
      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}
    end
  end

  # Restore session data from the cookie.
  def restore
    @original = read_cookie
    @data = unmarshal(@original) || {}
  end

  # Wait until close to write the session data cookie.
  def update; end

  # Write the session data cookie if it was loaded and has changed.
  def close
    if defined?(@data) && !@data.blank?
      updated = marshal(@data)
      raise CookieOverflow if updated.size > MAX
      write_cookie('value' => updated) unless updated == @original
    end
  end

  # Delete the session data by setting an expired cookie with no data.
  def delete
    @data = nil
    clear_old_cookie_value
    write_cookie('value' => nil, 'expires' => 1.year.ago)
  end

  private
    # Marshal a session hash into safe cookie data. Include an integrity hash.
    def marshal(session)
      verifier.generate(session)
    end

    # Unmarshal cookie data to a hash and verify its integrity.
    def unmarshal(cookie)
      if cookie
        verifier.verify(cookie)
      end
    rescue ActiveSupport::MessageVerifier::InvalidSignature
      delete
      raise TamperedWithCookie
    end

    # Read the session data cookie.
    def read_cookie
      @session.cgi.cookies[@cookie_options['name']].first
    end

    # CGI likes to make you hack.
    def write_cookie(options)
      cookie = CGI::Cookie.new(@cookie_options.merge(options))
      @session.cgi.send :instance_variable_set, '@output_cookies', [cookie]
    end

    # Clear cookie value so subsequent new_session doesn't reload old data.
    def clear_old_cookie_value
      @session.cgi.cookies[@cookie_options['name']].clear
    end
    
    def verifier
      if @secret.respond_to?(:call)
        key = @secret.call
      else
        key = @secret
      end
      ActiveSupport::MessageVerifier.new(key, @digest)
    end
end