diff options
Diffstat (limited to 'actionpack/lib/action_dispatch/middleware/cookies.rb')
| -rw-r--r-- | actionpack/lib/action_dispatch/middleware/cookies.rb | 492 |
1 files changed, 298 insertions, 194 deletions
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index b7687ca100..ea4156c972 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -1,15 +1,87 @@ -require 'active_support/core_ext/hash/keys' -require 'active_support/core_ext/module/attribute_accessors' -require 'active_support/core_ext/object/blank' -require 'active_support/key_generator' -require 'active_support/message_verifier' -require 'active_support/json' +# frozen_string_literal: true + +require "active_support/core_ext/hash/keys" +require "active_support/key_generator" +require "active_support/message_verifier" +require "active_support/json" +require "rack/utils" module ActionDispatch - class Request < Rack::Request + class Request def cookie_jar - env['action_dispatch.cookies'] ||= Cookies::CookieJar.build(self) + fetch_header("action_dispatch.cookies".freeze) do + self.cookie_jar = Cookies::CookieJar.build(self, cookies) + end + end + + # :stopdoc: + prepend Module.new { + def commit_cookie_jar! + cookie_jar.commit! + end + } + + def have_cookie_jar? + has_header? "action_dispatch.cookies".freeze + end + + def cookie_jar=(jar) + set_header "action_dispatch.cookies".freeze, jar + end + + def key_generator + get_header Cookies::GENERATOR_KEY + end + + def signed_cookie_salt + get_header Cookies::SIGNED_COOKIE_SALT + end + + def encrypted_cookie_salt + get_header Cookies::ENCRYPTED_COOKIE_SALT + end + + def encrypted_signed_cookie_salt + get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT + end + + def authenticated_encrypted_cookie_salt + get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT + end + + def use_authenticated_cookie_encryption + get_header Cookies::USE_AUTHENTICATED_COOKIE_ENCRYPTION + end + + def encrypted_cookie_cipher + get_header Cookies::ENCRYPTED_COOKIE_CIPHER + end + + def signed_cookie_digest + get_header Cookies::SIGNED_COOKIE_DIGEST + end + + def secret_token + get_header Cookies::SECRET_TOKEN + end + + def secret_key_base + get_header Cookies::SECRET_KEY_BASE + end + + def cookies_serializer + get_header Cookies::COOKIES_SERIALIZER + end + + def cookies_digest + get_header Cookies::COOKIES_DIGEST + end + + def cookies_rotations + get_header Cookies::COOKIES_ROTATIONS end + + # :startdoc: end # \Cookies are read and written through ActionController#cookies. @@ -28,18 +100,25 @@ module ActionDispatch # cookies[:lat_lon] = JSON.generate([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 } + # + # # Sets a cookie that expires at a specific time. + # cookies[:login] = { value: "XJ-122", expires: Time.utc(2020, 10, 15, 5) } # # # Sets a signed cookie, which prevents users from tampering with its value. - # # The cookie is signed by your app's `secrets.secret_key_base` value. # # It can be read using the signed method `cookies.signed[:name]` # cookies.signed[:user_id] = current_user.id # + # # Sets an encrypted cookie value before sending it to the client which + # # prevent users from reading and tampering with its value. + # # It can be read using the encrypted method `cookies.encrypted[:name]` + # cookies.encrypted[:discount] = 45 + # # # Sets a "permanent" cookie (which expires in 20 years from now). # cookies.permanent[:login] = "XJ-122" # # # You can also chain these methods: - # cookies.permanent.signed[:login] = "XJ-122" + # cookies.signed.permanent[:login] = "XJ-122" # # Examples of reading: # @@ -47,6 +126,7 @@ module ActionDispatch # cookies.size # => 2 # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37] # cookies.signed[:login] # => "XJ-122" + # cookies.encrypted[:discount] # => 45 # # Example for deleting: # @@ -56,7 +136,7 @@ module ActionDispatch # # cookies[:name] = { # value: 'a yummy cookie', - # expires: 1.year.from_now, + # expires: 1.year, # domain: 'domain.com' # } # @@ -79,7 +159,10 @@ module ActionDispatch # domain: %w(.example.com .example.org) # Allow the cookie # # for concrete domain names. # - # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object. + # * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly + # set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD. + # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 2. + # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time or ActiveSupport::Duration object. # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers. # Default is +false+. # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or @@ -90,10 +173,15 @@ module ActionDispatch 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 + AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt".freeze + USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption".freeze + ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher".freeze + SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest".freeze SECRET_TOKEN = "action_dispatch.secret_token".freeze SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze + COOKIES_ROTATIONS = "action_dispatch.cookies_rotations".freeze # Cookies can typically store 4096 bytes. MAX_COOKIE_SIZE = 4096 @@ -101,7 +189,7 @@ module ActionDispatch # Raised when storing more than 4K of session data. CookieOverflow = Class.new StandardError - # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed + # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed. module ChainedCookieJars # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example: # @@ -115,17 +203,17 @@ 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, @key_generator, @options) + @permanent ||= PermanentCookieJar.new(self) end # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed - # cookie was tampered with by the user (or a 3rd party), nil will be returned. + # cookie was tampered with by the user (or a 3rd party), +nil+ will be returned. # - # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, + # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # - # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. + # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+. # # Example: # @@ -134,66 +222,61 @@ module ActionDispatch # # cookies.signed[:discount] # => 45 def signed - @signed ||= - if @options[:upgrade_legacy_signed_cookies] - UpgradeLegacySignedCookieJar.new(self, @key_generator, @options) - else - SignedCookieJar.new(self, @key_generator, @options) - end + @signed ||= SignedKeyRotatingCookieJar.new(self) 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), nil will be returned. + # If the cookie was tampered with by the user (or a 3rd party), +nil+ will be returned. # - # If +secrets.secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, + # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set, # legacy cookies signed with the old key generator will be transparently upgraded. # - # This jar requires that you set a suitable secret for the verification on your app's +secrets.secret_key_base+. + # If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+ + # are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded. + # + # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+. # # Example: # # cookies.encrypted[:discount] = 45 - # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/ + # # => Set-Cookie: discount=DIQ7fw==--K3n//8vvnSbGq9dA--7Xh91HfLpwzbj1czhBiwOg==; path=/ # # cookies.encrypted[:discount] # => 45 def encrypted - @encrypted ||= - if @options[:upgrade_legacy_signed_cookies] - UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options) - else - EncryptedCookieJar.new(self, @key_generator, @options) - end + @encrypted ||= EncryptedKeyRotatingCookieJar.new(self) end # Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set. # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores. def signed_or_encrypted @signed_or_encrypted ||= - if @options[:secret_key_base].present? + if request.secret_key_base.present? encrypted else signed end end - end - # Passing the ActiveSupport::MessageEncryptor::NullSerializer downstream - # to the Message{Encryptor,Verifier} allows us to handle the - # (de)serialization step within the cookie jar, which gives us the - # opportunity to detect and migrate legacy cookies. - module VerifyAndUpgradeLegacySignedMessage - def initialize(*args) - super - @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token], serializer: ActiveSupport::MessageEncryptor::NullSerializer) - end + private - def verify_and_upgrade_legacy_signed_message(name, signed_message) - deserialize(name, @legacy_verifier.verify(signed_message)).tap do |value| - self[name] = { value: value } + def upgrade_legacy_signed_cookies? + request.secret_token.present? && request.secret_key_base.present? + end + + def upgrade_legacy_hmac_aes_cbc_cookies? + request.secret_key_base.present? && + request.encrypted_signed_cookie_salt.present? && + request.encrypted_cookie_salt.present? && + request.use_authenticated_cookie_encryption + end + + def encrypted_cookie_cipher + request.encrypted_cookie_cipher || "aes-256-gcm" + end + + def signed_cookie_digest + request.signed_cookie_digest || "SHA1" end - rescue ActiveSupport::MessageVerifier::InvalidSignature - nil - end end class CookieJar #:nodoc: @@ -213,38 +296,18 @@ module ActionDispatch # $& => example.local DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/ - def self.options_for_env(env) #:nodoc: - { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '', - encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '', - encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '', - secret_token: env[SECRET_TOKEN], - secret_key_base: env[SECRET_KEY_BASE], - upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?, - serializer: env[COOKIES_SERIALIZER], - digest: env[COOKIES_DIGEST] - } - end - - def self.build(request) - env = request.env - key_generator = env[GENERATOR_KEY] - options = options_for_env env - - host = request.host - secure = request.ssl? - - new(key_generator, host, secure, options).tap do |hash| - hash.update(request.cookies) + def self.build(req, cookies) + new(req).tap do |hash| + hash.update(cookies) end end - def initialize(key_generator, host = nil, secure = false, options = {}) - @key_generator = key_generator + attr_reader :request + + def initialize(request) @set_cookies = {} @delete_cookies = {} - @host = host - @secure = secure - @options = options + @request = request @cookies = {} @committed = false end @@ -280,21 +343,36 @@ module ActionDispatch self end - def handle_options(options) #:nodoc: + def update_cookies_from_jar + request_jar = @request.cookie_jar.instance_variable_get(:@cookies) + set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) } + + @cookies.update set_cookies if set_cookies + end + + def to_header + @cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; " + end + + def handle_options(options) # :nodoc: + if options[:expires].respond_to?(:from_now) + options[:expires] = options[:expires].from_now + end + options[:path] ||= "/" - if options[:domain] == :all || options[:domain] == 'all' - # if there is a provided tld length then we use it otherwise default domain regexp + if options[:domain] == :all || options[:domain] == "all" + # If there is a provided tld length then we use it otherwise default domain regexp. domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP - # if host is not ip and matches domain regexp + # If host is not ip and matches domain regexp. # (ip confirms to domain regexp so we explicitly check for ip) - options[:domain] = if (@host !~ /^[\d.]+$/) && (@host =~ domain_regexp) + options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp) ".#{$&}" 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.sub(/^\./, '') } + # If host matches one of the supplied domains without a dot in front of it. + options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") } end end @@ -306,12 +384,12 @@ module ActionDispatch value = options[:value] else value = options - options = { :value => value } + options = { value: value } end handle_options(options) - if @cookies[name.to_s] != value or options[:expires] + if @cookies[name.to_s] != value || options[:expires] @cookies[name.to_s] = value @set_cookies[name.to_s] = options @delete_cookies.delete(name.to_s) @@ -343,56 +421,91 @@ module ActionDispatch @delete_cookies[name.to_s] == options end - # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie + # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie. def clear(options = {}) - @cookies.each_key{ |k| delete(k, options) } + @cookies.each_key { |k| delete(k, options) } end def write(headers) - @set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) } - @delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) } - end - - def recycle! #:nodoc: - @set_cookies = {} - @delete_cookies = {} + if header = make_set_cookie_header(headers[HTTP_HEADER]) + headers[HTTP_HEADER] = header + end end - mattr_accessor :always_write_cookie - self.always_write_cookie = false + mattr_accessor :always_write_cookie, default: false private + + def escape(string) + ::Rack::Utils.escape(string) + end + + def make_set_cookie_header(header) + header = @set_cookies.inject(header) { |m, (k, v)| + if write_cookie?(v) + ::Rack::Utils.add_cookie_to_header(m, k, v) + else + m + end + } + @delete_cookies.inject(header) { |m, (k, v)| + ::Rack::Utils.add_remove_cookie_to_header(m, k, v) + } + end + def write_cookie?(cookie) - @secure || !cookie[:secure] || always_write_cookie + request.ssl? || !cookie[:secure] || always_write_cookie end end - class PermanentCookieJar #:nodoc: + class AbstractCookieJar # :nodoc: include ChainedCookieJars - def initialize(parent_jar, key_generator, options = {}) + def initialize(parent_jar) @parent_jar = parent_jar - @key_generator = key_generator - @options = options end def [](name) - @parent_jar[name.to_s] + if data = @parent_jar[name.to_s] + parse name, data + end end def []=(name, options) if options.is_a?(Hash) options.symbolize_keys! else - options = { :value => options } + options = { value: options } end - options[:expires] = 20.years.from_now + commit(options) @parent_jar[name] = options end + + protected + def request; @parent_jar.request; end + + private + def expiry_options(options) + if options[:expires].respond_to?(:from_now) + { expires_in: options[:expires] } + else + { expires_at: options[:expires] } + end + end + + def parse(name, data); data; end + def commit(options); end + end + + class PermanentCookieJar < AbstractCookieJar # :nodoc: + private + def commit(options) + options[:expires] = 20.years.from_now + end end - class JsonSerializer + class JsonSerializer # :nodoc: def self.load(value) ActiveSupport::JSON.decode(value) end @@ -402,24 +515,29 @@ module ActionDispatch end end - module SerializedCookieJars + module SerializedCookieJars # :nodoc: MARSHAL_SIGNATURE = "\x04\x08".freeze + SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer protected def needs_migration?(value) - @options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE) + request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE) end def serialize(value) serializer.dump(value) end - def deserialize(name, value) + def deserialize(name) + rotate = false + value = yield -> { rotate = true } + if value - if needs_migration?(value) - Marshal.load(value).tap do |v| - self[name] = { value: v } - end + case + when needs_migration?(value) + self[name] = Marshal.load(value) + when rotate + self[name] = serializer.load(value) else serializer.load(value) end @@ -427,7 +545,7 @@ module ActionDispatch end def serializer - serializer = @options[:serializer] || :marshal + serializer = request.cookies_serializer || :marshal case serializer when :marshal Marshal @@ -439,117 +557,100 @@ module ActionDispatch end def digest - @options[:digest] || 'SHA1' + request.cookies_digest || "SHA1" end end - class SignedCookieJar #:nodoc: - include ChainedCookieJars + class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc: include SerializedCookieJars - 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, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) - end + def initialize(parent_jar) + super - def [](name) - if signed_message = @parent_jar[name] - deserialize name, verify(signed_message) - end - end + secret = request.key_generator.generate_key(request.signed_cookie_salt) + @verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER) - def []=(name, options) - if options.is_a?(Hash) - options.symbolize_keys! - options[:value] = @verifier.generate(serialize(options[:value])) - else - options = { :value => @verifier.generate(serialize(options)) } + request.cookies_rotations.signed.each do |*secrets, **options| + @verifier.rotate(*secrets, serializer: SERIALIZER, **options) end - raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE - @parent_jar[name] = options + if upgrade_legacy_signed_cookies? + @verifier.rotate request.secret_token, serializer: SERIALIZER + end end private - def verify(signed_message) - @verifier.verify(signed_message) - rescue ActiveSupport::MessageVerifier::InvalidSignature - nil + def parse(name, signed_message) + deserialize(name) do |rotate| + @verifier.verified(signed_message, on_rotation: rotate) + end end - end - # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if - # secrets.secret_token and secrets.secret_key_base are both set. It reads - # legacy cookies signed with the old dummy key generator and re-saves - # them using the new key generator to provide a smooth upgrade path. - class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc: - include VerifyAndUpgradeLegacySignedMessage + def commit(options) + options[:value] = @verifier.generate(serialize(options[:value]), expiry_options(options)) - def [](name) - if signed_message = @parent_jar[name] - deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message) + raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE end - end end - class EncryptedCookieJar #:nodoc: - include ChainedCookieJars + class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc: include SerializedCookieJars - def initialize(parent_jar, key_generator, options = {}) - if ActiveSupport::LegacyKeyGenerator === key_generator - raise "You didn't set secrets.secret_key_base, which is required for this cookie jar. " + - "Read the upgrade documentation to learn more about this new config option." - end - - @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, digest: digest, serializer: ActiveSupport::MessageEncryptor::NullSerializer) - end + def initialize(parent_jar) + super - def [](name) - if encrypted_message = @parent_jar[name] - deserialize name, decrypt_and_verify(encrypted_message) + if request.use_authenticated_cookie_encryption + key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher) + secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len) + @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: SERIALIZER) + else + key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-cbc") + secret = request.key_generator.generate_key(request.encrypted_cookie_salt, key_len) + sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt) + @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: SERIALIZER) end - end - def []=(name, options) - if options.is_a?(Hash) - options.symbolize_keys! - else - options = { :value => options } + request.cookies_rotations.encrypted.each do |*secrets, **options| + @encryptor.rotate(*secrets, serializer: SERIALIZER, **options) end - options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value])) + if upgrade_legacy_hmac_aes_cbc_cookies? + legacy_cipher = "aes-256-cbc" + secret = request.key_generator.generate_key(request.encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(legacy_cipher)) + sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt) - raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE - @parent_jar[name] = options + @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER) + end + + if upgrade_legacy_signed_cookies? + @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, digest: digest, serializer: SERIALIZER) + end end private - def decrypt_and_verify(encrypted_message) - @encryptor.decrypt_and_verify(encrypted_message) - rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage - nil + def parse(name, encrypted_message) + deserialize(name) do |rotate| + @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate) + end + rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature + parse_legacy_signed_message(name, encrypted_message) end - end - # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore - # instead of EncryptedCookieJar if secrets.secret_token and secrets.secret_key_base - # are both set. It reads legacy cookies signed with the old dummy key generator and - # encrypts and re-saves them using the new key generator to provide a smooth upgrade path. - class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc: - include VerifyAndUpgradeLegacySignedMessage + def commit(options) + options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), expiry_options(options)) - def [](name) - if encrypted_or_signed_message = @parent_jar[name] - deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message) + raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE + end + + def parse_legacy_signed_message(name, legacy_signed_message) + if defined?(@legacy_verifier) + deserialize(name) do |rotate| + rotate.call + + @legacy_verifier.verified(legacy_signed_message) + end + end end - end end def initialize(app) @@ -557,9 +658,12 @@ module ActionDispatch end def call(env) + request = ActionDispatch::Request.new env + status, headers, body = @app.call(env) - if cookie_jar = env['action_dispatch.cookies'] + if request.have_cookie_jar? + cookie_jar = request.cookie_jar unless cookie_jar.committed? cookie_jar.write(headers) if headers[HTTP_HEADER].respond_to?(:join) |
