Merge pull request #13945 from rails/json_cookie_serializer_improvements
Cookies serializer improvements
This commit is contained in:
commit
de5ef15398
@ -1,3 +1,21 @@
|
||||
* Add new config option `config.action_dispatch.cookies_serializer` for
|
||||
specifying a serializer for the signed and encrypted cookie jars.
|
||||
|
||||
The possible values are:
|
||||
|
||||
* `:json` - serialize cookie values with `JSON`
|
||||
* `:marshal` - serialize cookie values with `Marshal`
|
||||
* `:hybrid` - transparently migrate existing `Marshal` cookie values to `JSON`
|
||||
|
||||
For new apps `:json` option is added by default and `:marshal` is used
|
||||
when no option is specified to maintain backwards compatibility.
|
||||
|
||||
*Łukasz Sarnacki*, *Matt Aimonetti*, *Guillermo Iguaran*, *Godfrey Chan*, *Rafael Mendonça França*
|
||||
|
||||
* `FlashHash` now behaves like a `HashWithIndifferentAccess`.
|
||||
|
||||
*Guillermo Iguaran*
|
||||
|
||||
* Set the `:shallow_path` scope option as each scope is generated rather than
|
||||
waiting until the `shallow` option is set. Also make the behavior of the
|
||||
`:shallow` resource option consistent with the behavior of the `shallow` method.
|
||||
@ -16,21 +34,6 @@
|
||||
|
||||
*Josh Jordan*
|
||||
|
||||
* Add `:serializer` option for `config.session_store :cookie_store`. This
|
||||
changes default serializer when using `:cookie_store`.
|
||||
|
||||
It is possible to pass:
|
||||
|
||||
* `:json` which is a secure wrapper on JSON using `JSON.parse` and
|
||||
`JSON.generate` methods with quirks mode;
|
||||
* `:marshal` which is a wrapper on Marshal;
|
||||
* serializer class with `load` and `dump` methods defined.
|
||||
|
||||
For new apps `:json` option is added by default and :marshal is used
|
||||
when no option is specified.
|
||||
|
||||
*Łukasz Sarnacki*, *Matt Aimonetti*
|
||||
|
||||
* Ensure that `request.filtered_parameters` is reset between calls to `process`
|
||||
in `ActionController::TestCase`.
|
||||
|
||||
|
@ -84,8 +84,6 @@ module Session
|
||||
autoload :CookieStore, 'action_dispatch/middleware/session/cookie_store'
|
||||
autoload :MemCacheStore, 'action_dispatch/middleware/session/mem_cache_store'
|
||||
autoload :CacheStore, 'action_dispatch/middleware/session/cache_store'
|
||||
autoload :JsonSerializer, 'action_dispatch/middleware/session/json_serializer'
|
||||
autoload :MarshalSerializer, 'action_dispatch/middleware/session/marshal_serializer'
|
||||
end
|
||||
|
||||
mattr_accessor :test_app
|
||||
|
@ -89,7 +89,7 @@ class Cookies
|
||||
ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
|
||||
SECRET_TOKEN = "action_dispatch.secret_token".freeze
|
||||
SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
|
||||
SESSION_SERIALIZER = "action_dispatch.session_serializer".freeze
|
||||
COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
|
||||
|
||||
# Cookies can typically store 4096 bytes.
|
||||
MAX_COOKIE_SIZE = 4096
|
||||
@ -181,7 +181,7 @@ def initialize(*args)
|
||||
|
||||
def verify_and_upgrade_legacy_signed_message(name, signed_message)
|
||||
@legacy_verifier.verify(signed_message).tap do |value|
|
||||
self[name] = value
|
||||
self[name] = { value: value }
|
||||
end
|
||||
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
||||
nil
|
||||
@ -212,7 +212,7 @@ def self.options_for_env(env) #:nodoc:
|
||||
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?,
|
||||
session_serializer: env[SESSION_SERIALIZER]
|
||||
serializer: env[COOKIES_SERIALIZER]
|
||||
}
|
||||
end
|
||||
|
||||
@ -374,28 +374,89 @@ def []=(name, options)
|
||||
end
|
||||
end
|
||||
|
||||
class JsonSerializer
|
||||
def self.load(value)
|
||||
JSON.parse(value, quirks_mode: true)
|
||||
end
|
||||
|
||||
def self.dump(value)
|
||||
JSON.generate(value, quirks_mode: true)
|
||||
end
|
||||
end
|
||||
|
||||
# Passing the 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.
|
||||
class NullSerializer
|
||||
def self.load(value)
|
||||
value
|
||||
end
|
||||
|
||||
def self.dump(value)
|
||||
value
|
||||
end
|
||||
end
|
||||
|
||||
module SerializedCookieJars
|
||||
MARSHAL_SIGNATURE = "\x04\x08".freeze
|
||||
|
||||
protected
|
||||
def needs_migration?(value)
|
||||
@options[:serializer] == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
|
||||
end
|
||||
|
||||
def serialize(name, value)
|
||||
serializer.dump(value)
|
||||
end
|
||||
|
||||
def deserialize(name, value)
|
||||
if value
|
||||
if needs_migration?(value)
|
||||
Marshal.load(value).tap do |v|
|
||||
self[name] = { value: v }
|
||||
end
|
||||
else
|
||||
serializer.load(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def serializer
|
||||
serializer = @options[:serializer] || :marshal
|
||||
case serializer
|
||||
when :marshal
|
||||
Marshal
|
||||
when :json, :hybrid
|
||||
JsonSerializer
|
||||
else
|
||||
serializer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class SignedCookieJar #:nodoc:
|
||||
include ChainedCookieJars
|
||||
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)
|
||||
@verifier = ActiveSupport::MessageVerifier.new(secret, serializer: NullSerializer)
|
||||
end
|
||||
|
||||
def [](name)
|
||||
if signed_message = @parent_jar[name]
|
||||
verify(signed_message)
|
||||
deserialize name, verify(signed_message)
|
||||
end
|
||||
end
|
||||
|
||||
def []=(name, options)
|
||||
if options.is_a?(Hash)
|
||||
options.symbolize_keys!
|
||||
options[:value] = @verifier.generate(options[:value])
|
||||
options[:value] = @verifier.generate(serialize(name, options[:value]))
|
||||
else
|
||||
options = { :value => @verifier.generate(options) }
|
||||
options = { :value => @verifier.generate(serialize(name, options)) }
|
||||
end
|
||||
|
||||
raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
|
||||
@ -419,13 +480,14 @@ class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
|
||||
|
||||
def [](name)
|
||||
if signed_message = @parent_jar[name]
|
||||
verify(signed_message) || verify_and_upgrade_legacy_signed_message(name, signed_message)
|
||||
deserialize(name, verify(signed_message)) || verify_and_upgrade_legacy_signed_message(name, signed_message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class EncryptedCookieJar #:nodoc:
|
||||
include ChainedCookieJars
|
||||
include SerializedCookieJars
|
||||
|
||||
def initialize(parent_jar, key_generator, options = {})
|
||||
if ActiveSupport::LegacyKeyGenerator === key_generator
|
||||
@ -437,12 +499,12 @@ def initialize(parent_jar, key_generator, options = {})
|
||||
@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, serializer: serializer)
|
||||
@encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: NullSerializer)
|
||||
end
|
||||
|
||||
def [](name)
|
||||
if encrypted_message = @parent_jar[name]
|
||||
decrypt_and_verify(encrypted_message)
|
||||
deserialize name, decrypt_and_verify(encrypted_message)
|
||||
end
|
||||
end
|
||||
|
||||
@ -452,7 +514,8 @@ def []=(name, options)
|
||||
else
|
||||
options = { :value => options }
|
||||
end
|
||||
options[:value] = @encryptor.encrypt_and_sign(options[:value])
|
||||
|
||||
options[:value] = @encryptor.encrypt_and_sign(serialize(name, options[:value]))
|
||||
|
||||
raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
|
||||
@parent_jar[name] = options
|
||||
@ -464,18 +527,6 @@ def decrypt_and_verify(encrypted_message)
|
||||
rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
|
||||
nil
|
||||
end
|
||||
|
||||
def serializer
|
||||
serializer = @options[:session_serializer] || :marshal
|
||||
case serializer
|
||||
when :marshal
|
||||
ActionDispatch::Session::MarshalSerializer
|
||||
when :json
|
||||
ActionDispatch::Session::JsonSerializer
|
||||
else
|
||||
serializer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
|
||||
@ -487,7 +538,7 @@ class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
|
||||
|
||||
def [](name)
|
||||
if encrypted_or_signed_message = @parent_jar[name]
|
||||
decrypt_and_verify(encrypted_or_signed_message) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message)
|
||||
deserialize(name, decrypt_and_verify(encrypted_or_signed_message)) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -1,3 +1,5 @@
|
||||
require 'active_support/core_ext/hash/keys'
|
||||
|
||||
module ActionDispatch
|
||||
class Request < Rack::Request
|
||||
# Access the contents of the flash. Use <tt>flash["notice"]</tt> to
|
||||
@ -50,13 +52,14 @@ def initialize(flash)
|
||||
end
|
||||
|
||||
def []=(k, v)
|
||||
k = k.to_s
|
||||
@flash[k] = v
|
||||
@flash.discard(k)
|
||||
v
|
||||
end
|
||||
|
||||
def [](k)
|
||||
@flash[k]
|
||||
@flash[k.to_s]
|
||||
end
|
||||
|
||||
# Convenience accessor for <tt>flash.now[:alert]=</tt>.
|
||||
@ -92,8 +95,8 @@ def to_session_value
|
||||
end
|
||||
|
||||
def initialize(flashes = {}, discard = []) #:nodoc:
|
||||
@discard = Set.new(discard)
|
||||
@flashes = flashes
|
||||
@discard = Set.new(stringify_array(discard))
|
||||
@flashes = flashes.stringify_keys
|
||||
@now = nil
|
||||
end
|
||||
|
||||
@ -106,17 +109,18 @@ def initialize_copy(other)
|
||||
end
|
||||
|
||||
def []=(k, v)
|
||||
k = k.to_s
|
||||
@discard.delete k
|
||||
@flashes[k] = v
|
||||
end
|
||||
|
||||
def [](k)
|
||||
@flashes[k]
|
||||
@flashes[k.to_s]
|
||||
end
|
||||
|
||||
def update(h) #:nodoc:
|
||||
@discard.subtract h.keys
|
||||
@flashes.update h
|
||||
@discard.subtract stringify_array(h.keys)
|
||||
@flashes.update h.stringify_keys
|
||||
self
|
||||
end
|
||||
|
||||
@ -129,6 +133,7 @@ def key?(name)
|
||||
end
|
||||
|
||||
def delete(key)
|
||||
key = key.to_s
|
||||
@discard.delete key
|
||||
@flashes.delete key
|
||||
self
|
||||
@ -155,7 +160,7 @@ def each(&block)
|
||||
|
||||
def replace(h) #:nodoc:
|
||||
@discard.clear
|
||||
@flashes.replace h
|
||||
@flashes.replace h.stringify_keys
|
||||
self
|
||||
end
|
||||
|
||||
@ -186,6 +191,7 @@ def now
|
||||
# flash.keep # keeps the entire flash
|
||||
# flash.keep(:notice) # keeps only the "notice" entry, the rest of the flash is discarded
|
||||
def keep(k = nil)
|
||||
k = k.to_s if k
|
||||
@discard.subtract Array(k || keys)
|
||||
k ? self[k] : self
|
||||
end
|
||||
@ -195,6 +201,7 @@ def keep(k = nil)
|
||||
# flash.discard # discard the entire flash at the end of the current action
|
||||
# flash.discard(:warning) # discard only the "warning" entry at the end of the current action
|
||||
def discard(k = nil)
|
||||
k = k.to_s if k
|
||||
@discard.merge Array(k || keys)
|
||||
k ? self[k] : self
|
||||
end
|
||||
@ -231,6 +238,12 @@ def notice=(message)
|
||||
def now_is_loaded?
|
||||
@now
|
||||
end
|
||||
|
||||
def stringify_array(array)
|
||||
array.map do |item|
|
||||
item.kind_of?(Symbol) ? item.to_s : item
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def initialize(app)
|
||||
|
@ -1,13 +0,0 @@
|
||||
module ActionDispatch
|
||||
module Session
|
||||
class JsonSerializer
|
||||
def self.load(value)
|
||||
JSON.parse(value, quirks_mode: true)
|
||||
end
|
||||
|
||||
def self.dump(value)
|
||||
JSON.generate(value, quirks_mode: true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
@ -1,14 +0,0 @@
|
||||
module ActionDispatch
|
||||
module Session
|
||||
class MarshalSerializer
|
||||
def self.load(value)
|
||||
Marshal.load(value)
|
||||
end
|
||||
|
||||
def self.dump(value)
|
||||
Marshal.dump(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -67,6 +67,16 @@ def test_from_session_value
|
||||
assert_equal({'flashes' => {'message' => 'Hello'}, 'discard' => %w[message]}, hash.to_session_value)
|
||||
end
|
||||
|
||||
def test_from_session_value_on_json_serializer
|
||||
decrypted_data = "{ \"session_id\":\"d98bdf6d129618fc2548c354c161cfb5\", \"flash\":{\"discard\":[], \"flashes\":{\"message\":\"hey you\"}} }"
|
||||
session = ActionDispatch::Cookies::JsonSerializer.load(decrypted_data)
|
||||
hash = Flash::FlashHash.from_session_value(session['flash'])
|
||||
|
||||
assert_equal({'discard' => %w[message], 'flashes' => { 'message' => 'hey you'}}, hash.to_session_value)
|
||||
assert_equal "hey you", hash[:message]
|
||||
assert_equal "hey you", hash["message"]
|
||||
end
|
||||
|
||||
def test_empty?
|
||||
assert @hash.empty?
|
||||
@hash['zomg'] = 'bears'
|
||||
|
@ -175,13 +175,13 @@ def test_keep_and_discard_return_values
|
||||
|
||||
assert_equal(:foo_indeed, flash.discard(:foo)) # valid key passed
|
||||
assert_nil flash.discard(:unknown) # non existent key passed
|
||||
assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard().to_hash) # nothing passed
|
||||
assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.discard(nil).to_hash) # nothing passed
|
||||
assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.discard().to_hash) # nothing passed
|
||||
assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.discard(nil).to_hash) # nothing passed
|
||||
|
||||
assert_equal(:foo_indeed, flash.keep(:foo)) # valid key passed
|
||||
assert_nil flash.keep(:unknown) # non existent key passed
|
||||
assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep().to_hash) # nothing passed
|
||||
assert_equal({:foo => :foo_indeed, :bar => :bar_indeed}, flash.keep(nil).to_hash) # nothing passed
|
||||
assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.keep().to_hash) # nothing passed
|
||||
assert_equal({"foo" => :foo_indeed, "bar" => :bar_indeed}, flash.keep(nil).to_hash) # nothing passed
|
||||
end
|
||||
|
||||
def test_redirect_to_with_alert
|
||||
|
@ -11,6 +11,16 @@
|
||||
require 'active_support/message_verifier'
|
||||
|
||||
class CookiesTest < ActionController::TestCase
|
||||
class CustomSerializer
|
||||
def self.load(value)
|
||||
value.to_s + " and loaded"
|
||||
end
|
||||
|
||||
def self.dump(value)
|
||||
value.to_s + " was dumped"
|
||||
end
|
||||
end
|
||||
|
||||
class TestController < ActionController::Base
|
||||
def authenticate
|
||||
cookies["user_name"] = "david"
|
||||
@ -359,9 +369,72 @@ def test_read_permanent_cookie
|
||||
assert_equal 'Jamie', @controller.send(:cookies).permanent[:user_name]
|
||||
end
|
||||
|
||||
def test_signed_cookie
|
||||
def test_signed_cookie_using_default_serializer
|
||||
get :set_signed_cookie
|
||||
assert_equal 45, @controller.send(:cookies).signed[:user_id]
|
||||
cookies = @controller.send :cookies
|
||||
assert_not_equal 45, cookies[:user_id]
|
||||
assert_equal 45, cookies.signed[:user_id]
|
||||
end
|
||||
|
||||
def test_signed_cookie_using_marshal_serializer
|
||||
@request.env["action_dispatch.cookies_serializer"] = :marshal
|
||||
get :set_signed_cookie
|
||||
cookies = @controller.send :cookies
|
||||
assert_not_equal 45, cookies[:user_id]
|
||||
assert_equal 45, cookies.signed[:user_id]
|
||||
end
|
||||
|
||||
def test_signed_cookie_using_json_serializer
|
||||
@request.env["action_dispatch.cookies_serializer"] = :json
|
||||
get :set_signed_cookie
|
||||
cookies = @controller.send :cookies
|
||||
assert_not_equal 45, cookies[:user_id]
|
||||
assert_equal 45, cookies.signed[:user_id]
|
||||
end
|
||||
|
||||
def test_signed_cookie_using_custom_serializer
|
||||
@request.env["action_dispatch.cookies_serializer"] = CustomSerializer
|
||||
get :set_signed_cookie
|
||||
assert_not_equal 45, cookies[:user_id]
|
||||
assert_equal '45 was dumped and loaded', cookies.signed[:user_id]
|
||||
end
|
||||
|
||||
def test_signed_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json
|
||||
@request.env["action_dispatch.cookies_serializer"] = :hybrid
|
||||
|
||||
key_generator = @request.env["action_dispatch.key_generator"]
|
||||
signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
|
||||
secret = key_generator.generate_key(signed_cookie_salt)
|
||||
|
||||
marshal_value = ActiveSupport::MessageVerifier.new(secret, serializer: Marshal).generate(45)
|
||||
@request.headers["Cookie"] = "user_id=#{marshal_value}"
|
||||
|
||||
get :get_signed_cookie
|
||||
|
||||
cookies = @controller.send :cookies
|
||||
assert_not_equal 45, cookies[:user_id]
|
||||
assert_equal 45, cookies.signed[:user_id]
|
||||
|
||||
verifier = ActiveSupport::MessageVerifier.new(secret, serializer: JSON)
|
||||
assert_equal 45, verifier.verify(@response.cookies['user_id'])
|
||||
end
|
||||
|
||||
def test_signed_cookie_using_hybrid_serializer_can_read_from_json_dumped_value
|
||||
@request.env["action_dispatch.cookies_serializer"] = :hybrid
|
||||
|
||||
key_generator = @request.env["action_dispatch.key_generator"]
|
||||
signed_cookie_salt = @request.env["action_dispatch.signed_cookie_salt"]
|
||||
secret = key_generator.generate_key(signed_cookie_salt)
|
||||
json_value = ActiveSupport::MessageVerifier.new(secret, serializer: JSON).generate(45)
|
||||
@request.headers["Cookie"] = "user_id=#{json_value}"
|
||||
|
||||
get :get_signed_cookie
|
||||
|
||||
cookies = @controller.send :cookies
|
||||
assert_not_equal 45, cookies[:user_id]
|
||||
assert_equal 45, cookies.signed[:user_id]
|
||||
|
||||
assert_nil @response.cookies["user_id"]
|
||||
end
|
||||
|
||||
def test_accessing_nonexistant_signed_cookie_should_not_raise_an_invalid_signature
|
||||
@ -369,7 +442,18 @@ def test_accessing_nonexistant_signed_cookie_should_not_raise_an_invalid_signatu
|
||||
assert_nil @controller.send(:cookies).signed[:non_existant_attribute]
|
||||
end
|
||||
|
||||
def test_encrypted_cookie
|
||||
def test_encrypted_cookie_using_default_serializer
|
||||
get :set_encrypted_cookie
|
||||
cookies = @controller.send :cookies
|
||||
assert_not_equal 'bar', cookies[:foo]
|
||||
assert_raise TypeError do
|
||||
cookies.signed[:foo]
|
||||
end
|
||||
assert_equal 'bar', cookies.encrypted[:foo]
|
||||
end
|
||||
|
||||
def test_encrypted_cookie_using_marshal_serializer
|
||||
@request.env["action_dispatch.cookies_serializer"] = :marshal
|
||||
get :set_encrypted_cookie
|
||||
cookies = @controller.send :cookies
|
||||
assert_not_equal 'bar', cookies[:foo]
|
||||
@ -379,33 +463,66 @@ def test_encrypted_cookie
|
||||
assert_equal 'bar', cookies.encrypted[:foo]
|
||||
end
|
||||
|
||||
class CustomJsonSerializer
|
||||
def self.load(value)
|
||||
JSON.load(value) + " and loaded"
|
||||
end
|
||||
|
||||
def self.dump(value)
|
||||
JSON.dump(value + " was dumped")
|
||||
end
|
||||
end
|
||||
|
||||
def test_encrypted_cookie_using_serializer_object
|
||||
@request.env["action_dispatch.session_serializer"] = CustomJsonSerializer
|
||||
get :set_encrypted_cookie
|
||||
assert_equal 'bar was dumped and loaded', cookies.encrypted[:foo]
|
||||
end
|
||||
|
||||
def test_encrypted_cookie_using_json_serializer
|
||||
@request.env["action_dispatch.session_serializer"] = :json
|
||||
@request.env["action_dispatch.cookies_serializer"] = :json
|
||||
get :set_encrypted_cookie
|
||||
cookies = @controller.send :cookies
|
||||
assert_not_equal 'bar', cookies[:foo]
|
||||
assert_raises TypeError do
|
||||
assert_raises ::JSON::ParserError do
|
||||
cookies.signed[:foo]
|
||||
end
|
||||
assert_equal 'bar', cookies.encrypted[:foo]
|
||||
end
|
||||
|
||||
def test_encrypted_cookie_using_custom_serializer
|
||||
@request.env["action_dispatch.cookies_serializer"] = CustomSerializer
|
||||
get :set_encrypted_cookie
|
||||
assert_not_equal 'bar', cookies.encrypted[:foo]
|
||||
assert_equal 'bar was dumped and loaded', cookies.encrypted[:foo]
|
||||
end
|
||||
|
||||
def test_encrypted_cookie_using_hybrid_serializer_can_migrate_marshal_dumped_value_to_json
|
||||
@request.env["action_dispatch.cookies_serializer"] = :hybrid
|
||||
|
||||
key_generator = @request.env["action_dispatch.key_generator"]
|
||||
encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
|
||||
encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
|
||||
secret = key_generator.generate_key(encrypted_cookie_salt)
|
||||
sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
|
||||
|
||||
marshal_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: Marshal).encrypt_and_sign("bar")
|
||||
@request.headers["Cookie"] = "foo=#{marshal_value}"
|
||||
|
||||
get :get_encrypted_cookie
|
||||
|
||||
cookies = @controller.send :cookies
|
||||
assert_not_equal "bar", cookies[:foo]
|
||||
assert_equal "bar", cookies.encrypted[:foo]
|
||||
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON)
|
||||
assert_equal "bar", encryptor.decrypt_and_verify(@response.cookies["foo"])
|
||||
end
|
||||
|
||||
def test_encrypted_cookie_using_hybrid_serializer_can_read_from_json_dumped_value
|
||||
@request.env["action_dispatch.cookies_serializer"] = :hybrid
|
||||
|
||||
key_generator = @request.env["action_dispatch.key_generator"]
|
||||
encrypted_cookie_salt = @request.env["action_dispatch.encrypted_cookie_salt"]
|
||||
encrypted_signed_cookie_salt = @request.env["action_dispatch.encrypted_signed_cookie_salt"]
|
||||
secret = key_generator.generate_key(encrypted_cookie_salt)
|
||||
sign_secret = key_generator.generate_key(encrypted_signed_cookie_salt)
|
||||
json_value = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: JSON).encrypt_and_sign("bar")
|
||||
@request.headers["Cookie"] = "foo=#{json_value}"
|
||||
|
||||
get :get_encrypted_cookie
|
||||
|
||||
cookies = @controller.send :cookies
|
||||
assert_not_equal "bar", cookies[:foo]
|
||||
assert_equal "bar", cookies.encrypted[:foo]
|
||||
|
||||
assert_nil @response.cookies["foo"]
|
||||
end
|
||||
|
||||
def test_accessing_nonexistant_encrypted_cookie_should_not_raise_invalid_message
|
||||
get :set_encrypted_cookie
|
||||
assert_nil @controller.send(:cookies).encrypted[:non_existant_attribute]
|
||||
@ -721,8 +838,6 @@ def test_cookies_hash_is_indifferent_access
|
||||
assert_equal "dhh", cookies['user_name']
|
||||
end
|
||||
|
||||
|
||||
|
||||
def test_setting_request_cookies_is_indifferent_access
|
||||
cookies.clear
|
||||
cookies[:user_name] = "andrew"
|
||||
|
@ -346,10 +346,8 @@ for detailed changes.
|
||||
params "deep munging" that was used to address security vulnerability
|
||||
CVE-2013-0155. ([Pull Request](https://github.com/rails/rails/pull/13188))
|
||||
|
||||
* Added `:serializer` option for `config.session_store :cookie_store`. This
|
||||
changes default serializer when using
|
||||
`:cookie_store`. ([Pull Request](https://github.com/rails/rails/pull/13692))
|
||||
|
||||
* New config option `config.action_dispatch.cookies_serializer` for specifying
|
||||
a serializer for the signed and encrypted cookie jars. (Pull Requests [1](https://github.com/rails/rails/pull/13692), [2](https://github.com/rails/rails/pull/13945) / [More Details](upgrading_ruby_on_rails.html#cookies-serializer))
|
||||
|
||||
Action Mailer
|
||||
-------------
|
||||
|
@ -381,22 +381,6 @@ You can also pass a `:domain` key and specify the domain name for the cookie:
|
||||
YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"
|
||||
```
|
||||
|
||||
You can pass `:serializer` key to specify serializer for serializing session:
|
||||
|
||||
```ruby
|
||||
YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', serializer: :json
|
||||
```
|
||||
|
||||
The default serializer for new application is `:json`. For compatibility with
|
||||
old applications `:marshal` is used when `serializer` option is not specified.
|
||||
|
||||
It is also possible to pass a custom serializer class with `load` and `dump`
|
||||
public methods defined:
|
||||
|
||||
```ruby
|
||||
YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', serializer: MyCustomSerializer
|
||||
```
|
||||
|
||||
Rails sets up (for the CookieStore) a secret key used for signing the session data. This can be changed in `config/initializers/secret_token.rb`
|
||||
|
||||
```ruby
|
||||
@ -588,6 +572,38 @@ end
|
||||
|
||||
Note that while for session values you set the key to `nil`, to delete a cookie value you should use `cookies.delete(:key)`.
|
||||
|
||||
Rails also provides a signed cookie jar and an encrypted cookie jar for storing
|
||||
sensitive data. The signed cookie jar appends a cryptographic signature on the
|
||||
cookie values to protect their integrity. The encrypted cookie jar encrypts the
|
||||
values in addition to signing them, so that they cannot be read by the end user.
|
||||
Refer to the [API documentation](http://api.rubyonrails.org/classes/ActionDispatch/Cookies.html)
|
||||
for more details.
|
||||
|
||||
These special cookie jars use a serializer to serialize the assigned values into
|
||||
strings and deserializes them into Ruby objects on read.
|
||||
|
||||
You can specify what serializer to use:
|
||||
|
||||
```ruby
|
||||
Rails.application.config.action_dispatch.cookies_serializer = :json
|
||||
```
|
||||
|
||||
The default serializer for new applications is `:json`. For compatibility with
|
||||
old applications with existing cookies, `:marshal` is used when `serializer`
|
||||
option is not specified.
|
||||
|
||||
You may also set this option to `:hybrid`, in which case Rails would transparently
|
||||
deserialize existing (`Marshal`-serialized) cookies on read and re-write them in
|
||||
the `JSON` format. This is useful for migrating existing applications to the
|
||||
`:json` serializer.
|
||||
|
||||
It is also possible to pass a custom serializer that responds to `load` and
|
||||
`dump`:
|
||||
|
||||
```ruby
|
||||
Rails.application.config.action_dispatch.cookies_serializer = MyCustomSerializer
|
||||
```
|
||||
|
||||
Rendering XML and JSON data
|
||||
---------------------------
|
||||
|
||||
|
@ -98,6 +98,19 @@ If your test helper contains a call to
|
||||
is now done automatically when you `require 'test_help'`, although
|
||||
leaving this line in your helper is not harmful in any way.
|
||||
|
||||
### Cookies serializer
|
||||
|
||||
Applications created before Rails 4.1 uses `Marshal` to serialize cookie values into
|
||||
the signed and encrypted cookie jars. If you want to use the new `JSON`-based format
|
||||
in your application, you can add an initializer file with the following content:
|
||||
|
||||
```ruby
|
||||
Rails.application.config.cookies_serializer :hybrid
|
||||
```
|
||||
|
||||
This would transparently migrate your existing `Marshal`-serialized cookies into the
|
||||
new `JSON`-based format.
|
||||
|
||||
### Changes in JSON handling
|
||||
|
||||
There are a few major changes related to JSON handling in Rails 4.1.
|
||||
|
@ -206,7 +206,7 @@ def env_config
|
||||
"action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt,
|
||||
"action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt,
|
||||
"action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt,
|
||||
"action_dispatch.session_serializer" => config.session_options[:serializer]
|
||||
"action_dispatch.cookies_serializer" => config.action_dispatch.cookies_serializer
|
||||
})
|
||||
end
|
||||
end
|
||||
|
3
railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb
Normal file
3
railties/lib/rails/generators/rails/app/templates/config/initializers/cookies_serializer.rb
Normal file
@ -0,0 +1,3 @@
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
Rails.application.config.action_dispatch.cookies_serializer = :json
|
@ -1,3 +1,3 @@
|
||||
# Be sure to restart your server when you modify this file.
|
||||
|
||||
Rails.application.config.session_store :cookie_store, key: <%= "'_#{app_name}_session'" %>, serializer: :json
|
||||
Rails.application.config.session_store :cookie_store, key: <%= "'_#{app_name}_session'" %>
|
||||
|
@ -433,7 +433,7 @@ def test_no_active_record_or_test_unit_if_skips_given
|
||||
def test_new_hash_style
|
||||
run_generator [destination_root]
|
||||
assert_file "config/initializers/session_store.rb" do |file|
|
||||
assert_match(/config.session_store :cookie_store, key: '_.+_session', serializer: :json/, file)
|
||||
assert_match(/config.session_store :cookie_store, key: '_.+_session'/, file)
|
||||
end
|
||||
end
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user