diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index fc05ae3cec..60b0195510 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -49,6 +49,20 @@ *Alessandro Diaferia* +* Add `:serializer` option for `config.session_store :cookie_store`. This + changes default serializer when using `:cookie_store` to + `ActionDispatch::Session::MarshalSerializer` which is wrapper on Marshal. + + It is also possible to pass: + + * `:json_serializer` which is secure wrapper on JSON using `JSON.parse` and + `JSON.generate` methods with quirks mode; + * any other Symbol or String like `:my_custom_serializer` which will be + camelized and constantized in `ActionDispatch::Session` namespace; + * serializer object with `load` and `dump` methods defined. + + *Ɓukasz Sarnacki* + * Allow an absolute controller path inside a module scope. Fixes #12777. Example: diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb index 920e651b08..36dcca2905 100644 --- a/actionpack/lib/action_dispatch.rb +++ b/actionpack/lib/action_dispatch.rb @@ -82,10 +82,12 @@ module ActionDispatch end module Session - autoload :AbstractStore, 'action_dispatch/middleware/session/abstract_store' - 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 :AbstractStore, 'action_dispatch/middleware/session/abstract_store' + 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 diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb index fe110d7938..f9f034952e 100644 --- a/actionpack/lib/action_dispatch/middleware/cookies.rb +++ b/actionpack/lib/action_dispatch/middleware/cookies.rb @@ -89,6 +89,7 @@ module ActionDispatch 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 can typically store 4096 bytes. MAX_COOKIE_SIZE = 4096 @@ -210,7 +211,8 @@ module ActionDispatch 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? + upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?, + session_serializer: env[SESSION_SERIALIZER] } end @@ -435,7 +437,7 @@ module ActionDispatch @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) + @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, serializer: serializer) end def [](name) @@ -462,6 +464,16 @@ module ActionDispatch rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage nil end + + def serializer + serializer = @options[:session_serializer] || :marshal_serializer + case serializer + when Symbol, String + ActionDispatch::Session.const_get(serializer.to_s.camelize) + else + serializer + end + end end # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore diff --git a/actionpack/lib/action_dispatch/middleware/session/json_serializer.rb b/actionpack/lib/action_dispatch/middleware/session/json_serializer.rb new file mode 100644 index 0000000000..d341853f7a --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/session/json_serializer.rb @@ -0,0 +1,13 @@ +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 diff --git a/actionpack/lib/action_dispatch/middleware/session/marshal_serializer.rb b/actionpack/lib/action_dispatch/middleware/session/marshal_serializer.rb new file mode 100644 index 0000000000..26622f682d --- /dev/null +++ b/actionpack/lib/action_dispatch/middleware/session/marshal_serializer.rb @@ -0,0 +1,14 @@ +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 + diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb index 91ac13e7c6..b19ce905f5 100644 --- a/actionpack/test/dispatch/cookies_test.rb +++ b/actionpack/test/dispatch/cookies_test.rb @@ -379,6 +379,39 @@ class CookiesTest < ActionController::TestCase assert_equal 'bar', cookies.encrypted[:foo] end + class ActionDispatch::Session::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_custom_json_serializer + @request.env["action_dispatch.session_serializer"] = :custom_json_serializer + get :set_encrypted_cookie + assert_equal 'bar was dumped and loaded', cookies.encrypted[:foo] + end + + def test_encrypted_cookie_using_serializer_object + @request.env["action_dispatch.session_serializer"] = ActionDispatch::Session::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_serializer + get :set_encrypted_cookie + cookies = @controller.send :cookies + assert_not_equal 'bar', cookies[:foo] + assert_raises TypeError do + cookies.signed[:foo] + end + assert_equal 'bar', cookies.encrypted[: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] diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb index 7773611e11..b019ad0dec 100644 --- a/activesupport/lib/active_support/message_encryptor.rb +++ b/activesupport/lib/active_support/message_encryptor.rb @@ -12,7 +12,7 @@ module ActiveSupport # This can be used in situations similar to the MessageVerifier, but # where you don't want users to be able to determine the value of the payload. # - # salt = SecureRandom.random_bytes(64) + # salt = SecureRandom.random_bytes(64) # key = ActiveSupport::KeyGenerator.new('password').generate_key(salt) # => "\x89\xE0\x156\xAC..." # crypt = ActiveSupport::MessageEncryptor.new(key) # => # # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..." diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md index c55637eb0a..0234120b45 100644 --- a/guides/source/action_controller_overview.md +++ b/guides/source/action_controller_overview.md @@ -381,6 +381,28 @@ 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_serializer +``` + +Default serializer is `:marshal_serializer`. When Symbol or String is passed it +will look for appropriate class in `ActionDispatch::Session` namespace, so +passing `:my_custom_serializer` would load +`ActionDispatch::Session::MyCustomSerializer`. + +```ruby +YourApp::Application.config.session_store :cookie_store, key: '_your_app_session', serializer: :my_custom_serializer +``` + +It is also possible to pass serializer object with defined `load` and `dump` +public methods: + +```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 diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb index 05acd78d98..36432e56ba 100644 --- a/railties/lib/rails/application.rb +++ b/railties/lib/rails/application.rb @@ -205,7 +205,8 @@ module Rails "action_dispatch.http_auth_salt" => config.action_dispatch.http_auth_salt, "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.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt, + "action_dispatch.session_serializer" => config.session_options[:serializer] }) end end diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt index 2bb9b82c61..923d423287 100644 --- a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt +++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt @@ -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'" %> +Rails.application.config.session_store :cookie_store, key: <%= "'_#{app_name}_session'" %>, serializer: :json_serializer diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb index ddecee2ca1..8aa306c8e0 100644 --- a/railties/test/generators/app_generator_test.rb +++ b/railties/test/generators/app_generator_test.rb @@ -433,7 +433,7 @@ class AppGeneratorTest < Rails::Generators::TestCase 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'/, file) + assert_match(/config.session_store :cookie_store, key: '_.+_session', serializer: :json_serializer/, file) end end