diff options
Diffstat (limited to 'actionpack/lib/action_dispatch/middleware/cookies.rb')
-rw-r--r-- | actionpack/lib/action_dispatch/middleware/cookies.rb | 162 |
1 files changed, 105 insertions, 57 deletions
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index 25f1db8228..2f148752cb 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -1,8 +1,9 @@ -require 'active_support/core_ext/object/blank' require 'active_support/core_ext/hash/keys' +require 'active_support/core_ext/module/attribute_accessors' +require 'active_support/message_verifier' module ActionDispatch - class Request + class Request < Rack::Request def cookie_jar env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self) end @@ -24,11 +25,11 @@ module ActionDispatch # cookies[:lat_lon] = [47.68, -122.37] # # # Sets a cookie that expires in 1 hour. - # cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now } + # cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now } # - # # Sets a signed cookie, which prevents a user from tampering with its value. - # # The cookie is signed by your app's <tt>config.secret_token</tt> value. - # # Rails generates this value by default when you create a new Rails app. + # # Sets a signed cookie, which prevents users from tampering with its value. + # # The cookie is signed by your app's <tt>config.secret_key_base</tt> value. + # # It can be read using the signed method <tt>cookies.signed[:key]</tt> # cookies.signed[:user_id] = current_user.id # # # Sets a "permanent" cookie (which expires in 20 years from now). @@ -39,9 +40,10 @@ module ActionDispatch # # Examples for reading: # - # cookies[:user_name] # => "david" - # cookies.size # => 2 - # cookies[:lat_lon] # => [47.68, -122.37] + # cookies[:user_name] # => "david" + # cookies.size # => 2 + # cookies[:lat_lon] # => [47.68, -122.37] + # cookies.signed[:login] # => "XJ-122" # # Example for deleting: # @@ -50,12 +52,12 @@ module ActionDispatch # Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie: # # cookies[:key] = { - # :value => 'a yummy cookie', - # :expires => 1.year.from_now, - # :domain => 'domain.com' + # value: 'a yummy cookie', + # expires: 1.year.from_now, + # domain: 'domain.com' # } # - # cookies.delete(:key, :domain => 'domain.com') + # cookies.delete(:key, domain: 'domain.com') # # The option symbols for setting cookies are: # @@ -68,8 +70,8 @@ module ActionDispatch # to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with # <tt>:all</tt> again when deleting keys. # - # :domain => nil # Does not sets cookie domain. (default) - # :domain => :all # Allow the cookie for the top most level + # domain: nil # Does not sets cookie domain. (default) + # domain: :all # Allow the cookie for the top most level # domain and subdomains. # # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object. @@ -78,11 +80,15 @@ module ActionDispatch # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or # only HTTP. Defaults to +false+. class Cookies - HTTP_HEADER = "Set-Cookie".freeze + HTTP_HEADER = "Set-Cookie".freeze + GENERATOR_KEY = "action_dispatch.key_generator".freeze + SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze + ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze + ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze TOKEN_KEY = "action_dispatch.secret_token".freeze # Raised when storing more than 4K of session data. - class CookieOverflow < StandardError; end + CookieOverflow = Class.new StandardError class CookieJar #:nodoc: include Enumerable @@ -102,22 +108,28 @@ module ActionDispatch DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/ def self.build(request) - secret = request.env[TOKEN_KEY] + env = request.env + key_generator = env[GENERATOR_KEY] + options = { signed_cookie_salt: env[SIGNED_COOKIE_SALT], + encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT], + encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT], + token_key: env[TOKEN_KEY] } + host = request.host secure = request.ssl? - new(secret, host, secure).tap do |hash| + new(key_generator, host, secure, options).tap do |hash| hash.update(request.cookies) end end - def initialize(secret = nil, host = nil, secure = false) - @secret = secret + def initialize(key_generator, host = nil, secure = false, options = {}) + @key_generator = key_generator @set_cookies = {} @delete_cookies = {} @host = host @secure = secure - @closed = false + @options = options @cookies = {} end @@ -154,7 +166,7 @@ module ActionDispatch end elsif options[:domain].is_a? Array # if host matches one of the supplied domains without a dot in front of it - options[:domain] = options[:domain].find {|domain| @host.include? domain[/^\.?(.*)$/, 1] } + options[:domain] = options[:domain].find {|domain| @host.include? domain.sub(/^\./, '') } end end @@ -169,12 +181,14 @@ module ActionDispatch options = { :value => value } end - @cookies[key.to_s] = value - handle_options(options) - @set_cookies[key.to_s] = options - @delete_cookies.delete(key.to_s) + if @cookies[key.to_s] != value or options[:expires] + @cookies[key.to_s] = value + @set_cookies[key.to_s] = options + @delete_cookies.delete(key.to_s) + end + value end @@ -182,8 +196,9 @@ module ActionDispatch # and setting its expiration date into the past. Like <tt>[]=</tt>, you can pass in # an options hash to delete cookies with extra data such as a <tt>:path</tt>. def delete(key, options = {}) - options.symbolize_keys! + return unless @cookies.has_key? key.to_s + options.symbolize_keys! handle_options(options) value = @cookies.delete(key.to_s) @@ -217,7 +232,7 @@ module ActionDispatch # cookies.permanent.signed[:remember_me] = current_user.id # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT def permanent - @permanent ||= PermanentCookieJar.new(self, @secret) + @permanent ||= PermanentCookieJar.new(self, @key_generator, @options) end # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from @@ -225,7 +240,7 @@ module ActionDispatch # cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will # be raised. # - # This jar requires that you set a suitable secret for the verification on your app's config.secret_token. + # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. # # Example: # @@ -234,7 +249,28 @@ module ActionDispatch # # cookies.signed[:discount] # => 45 def signed - @signed ||= SignedCookieJar.new(self, @secret) + @signed ||= SignedCookieJar.new(self, @key_generator, @options) + end + + # Only needed for supporting the +UpgradeSignatureToEncryptionCookieStore+, users and plugin authors should not use this + def signed_using_old_secret #:nodoc: + @signed_using_old_secret ||= SignedCookieJar.new(self, ActiveSupport::DummyKeyGenerator.new(@options[:token_key]), @options) + end + + # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read. + # If the cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception + # will be raised. + # + # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+. + # + # Example: + # + # cookies.encrypted[:discount] = 45 + # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/ + # + # cookies.encrypted[:discount] # => 45 + def encrypted + @encrypted ||= EncryptedCookieJar.new(self, @key_generator, @options) end def write(headers) @@ -258,8 +294,10 @@ module ActionDispatch end class PermanentCookieJar < CookieJar #:nodoc: - def initialize(parent_jar, secret) - @parent_jar, @secret = parent_jar, secret + def initialize(parent_jar, key_generator, options = {}) + @parent_jar = parent_jar + @key_generator = key_generator + @options = options end def []=(key, options) @@ -273,10 +311,6 @@ module ActionDispatch @parent_jar[key] = options end - def signed - @signed ||= SignedCookieJar.new(self, @secret) - end - def method_missing(method, *arguments, &block) @parent_jar.send(method, *arguments, &block) end @@ -284,11 +318,11 @@ module ActionDispatch class SignedCookieJar < CookieJar #:nodoc: MAX_COOKIE_SIZE = 4096 # Cookies can typically store 4096 bytes. - SECRET_MIN_LENGTH = 30 # Characters - def initialize(parent_jar, secret) - ensure_secret_secure(secret) + def initialize(parent_jar, key_generator, options = {}) @parent_jar = parent_jar + @options = options + secret = key_generator.generate_key(@options[:signed_cookie_salt]) @verifier = ActiveSupport::MessageVerifier.new(secret) end @@ -315,26 +349,41 @@ module ActionDispatch def method_missing(method, *arguments, &block) @parent_jar.send(method, *arguments, &block) end + end - protected - - # 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) - if secret.blank? - raise ArgumentError, "A secret is required to generate an " + - "integrity hash for cookie session data. Use " + - "config.secret_token = \"some secret phrase of at " + - "least #{SECRET_MIN_LENGTH} characters\"" + - "in config/initializers/secret_token.rb" + class EncryptedCookieJar < SignedCookieJar #:nodoc: + def initialize(parent_jar, key_generator, options = {}) + if ActiveSupport::DummyKeyGenerator === key_generator + raise "Encrypted Cookies must be used in conjunction with config.secret_key_base." + + "Set config.secret_key_base in config/initializers/secret_token.rb" end - if secret.length < SECRET_MIN_LENGTH - raise ArgumentError, "Secret should be something secure, " + - "like \"#{SecureRandom.hex(16)}\". The value you " + - "provided, \"#{secret}\", is shorter than the minimum length " + - "of #{SECRET_MIN_LENGTH} characters" + @parent_jar = parent_jar + @options = options + secret = key_generator.generate_key(@options[:encrypted_cookie_salt]) + sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt]) + @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret) + end + + def [](name) + if encrypted_message = @parent_jar[name] + @encryptor.decrypt_and_verify(encrypted_message) + end + rescue ActiveSupport::MessageVerifier::InvalidSignature, + ActiveSupport::MessageVerifier::InvalidMessage + nil + end + + def []=(key, options) + if options.is_a?(Hash) + options.symbolize_keys! + else + options = { :value => options } end + options[:value] = @encryptor.encrypt_and_sign(options[:value]) + + raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE + @parent_jar[key] = options end end @@ -343,7 +392,6 @@ module ActionDispatch end def call(env) - cookie_jar = nil status, headers, body = @app.call(env) if cookie_jar = env['action_dispatch.cookies'] |