mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
Change MessageEncryptor default serializer to JSON for Rails 7.1
These changes include adding a hybrid serializer class named JsonWithMarshalFallback in order for existing apps to have an upgrade path from Marshal to JSON.
This commit is contained in:
parent
32015b6f36
commit
5d0c2b0dd2
15 changed files with 553 additions and 13 deletions
|
@ -1,3 +1,9 @@
|
|||
* Change default serialization format of `MessageEncryptor` from `Marshal` to `JSON` for Rails 7.1.
|
||||
|
||||
Existing apps are provided with an upgrade path to migrate to `JSON` as described in `guides/source/upgrading_ruby_on_rails.md`
|
||||
|
||||
*Zack Deveau* and *Martin Gingras*
|
||||
|
||||
* Add `ActiveSupport::TestCase#stub_const` to stub a constant for the duration of a yield.
|
||||
|
||||
*DHH*
|
||||
|
|
|
@ -66,6 +66,7 @@ module ActiveSupport
|
|||
autoload :Gzip
|
||||
autoload :Inflector
|
||||
autoload :JSON
|
||||
autoload :JsonWithMarshalFallback
|
||||
autoload :KeyGenerator
|
||||
autoload :MessageEncryptor
|
||||
autoload :MessageVerifier
|
||||
|
|
|
@ -28,6 +28,7 @@ module ActiveSupport
|
|||
data
|
||||
end
|
||||
end
|
||||
alias_method :load, :decode
|
||||
|
||||
# Returns the class of the error that will be raised when there is an
|
||||
# error in decoding JSON. Using this method means you won't directly
|
||||
|
|
|
@ -18,8 +18,11 @@ module ActiveSupport
|
|||
#
|
||||
# ActiveSupport::JSON.encode({ team: 'rails', players: '36' })
|
||||
# # => "{\"team\":\"rails\",\"players\":\"36\"}"
|
||||
def self.encode(value, options = nil)
|
||||
Encoding.json_encoder.new(options).encode(value)
|
||||
class << self
|
||||
def encode(value, options = nil)
|
||||
Encoding.json_encoder.new(options).encode(value)
|
||||
end
|
||||
alias_method :dump, :encode
|
||||
end
|
||||
|
||||
module Encoding # :nodoc:
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module ActiveSupport
|
||||
class JsonWithMarshalFallback
|
||||
MARSHAL_SIGNATURE = "\x04\x08"
|
||||
|
||||
cattr_accessor :fallback_to_marshal_deserialization, instance_accessor: false, default: true
|
||||
cattr_accessor :use_marshal_serialization, instance_accessor: false, default: true
|
||||
|
||||
class << self
|
||||
def logger
|
||||
if defined?(Rails) && Rails.respond_to?(:logger)
|
||||
Rails.logger
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def dump(value)
|
||||
if self.use_marshal_serialization
|
||||
Marshal.dump(value)
|
||||
else
|
||||
JSON.encode(value)
|
||||
end
|
||||
end
|
||||
|
||||
def load(value)
|
||||
if self.fallback_to_marshal_deserialization
|
||||
if value.starts_with?(MARSHAL_SIGNATURE)
|
||||
logger.warn("JsonWithMarshalFallback: Marshal load fallback occurred.") if logger
|
||||
Marshal.load(value)
|
||||
else
|
||||
JSON.decode(value)
|
||||
end
|
||||
else
|
||||
raise ::JSON::ParserError if value.start_with?(MARSHAL_SIGNATURE)
|
||||
JSON.decode(value)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -87,6 +87,7 @@ module ActiveSupport
|
|||
prepend Messages::Rotator::Encryptor
|
||||
|
||||
cattr_accessor :use_authenticated_message_encryption, instance_accessor: false, default: false
|
||||
cattr_accessor :default_message_encryptor_serializer, instance_accessor: false, default: :marshal
|
||||
|
||||
class << self
|
||||
def default_cipher # :nodoc:
|
||||
|
@ -142,14 +143,21 @@ module ActiveSupport
|
|||
# <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-gcm'.
|
||||
# * <tt>:digest</tt> - String of digest to use for signing. Default is
|
||||
# +SHA1+. Ignored when using an AEAD cipher like 'aes-256-gcm'.
|
||||
# * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
|
||||
# * <tt>:serializer</tt> - Object serializer to use. Default is +JSON+.
|
||||
def initialize(secret, sign_secret = nil, cipher: nil, digest: nil, serializer: nil)
|
||||
@secret = secret
|
||||
@sign_secret = sign_secret
|
||||
@cipher = cipher || self.class.default_cipher
|
||||
@digest = digest || "SHA1" unless aead_mode?
|
||||
@verifier = resolve_verifier
|
||||
@serializer = serializer || Marshal
|
||||
@serializer = serializer ||
|
||||
if @@default_message_encryptor_serializer.equal?(:marshal)
|
||||
Marshal
|
||||
elsif @@default_message_encryptor_serializer.equal?(:hybrid)
|
||||
JsonWithMarshalFallback
|
||||
elsif @@default_message_encryptor_serializer.equal?(:json)
|
||||
JSON
|
||||
end
|
||||
end
|
||||
|
||||
# Encrypt and sign a message. We need to sign the message in order to avoid
|
||||
|
@ -170,6 +178,14 @@ module ActiveSupport
|
|||
end
|
||||
|
||||
private
|
||||
def serialize(value)
|
||||
@serializer.dump(value)
|
||||
end
|
||||
|
||||
def deserialize(value)
|
||||
@serializer.load(value)
|
||||
end
|
||||
|
||||
def _encrypt(value, **metadata_options)
|
||||
cipher = new_cipher
|
||||
cipher.encrypt
|
||||
|
@ -179,7 +195,7 @@ module ActiveSupport
|
|||
iv = cipher.random_iv
|
||||
cipher.auth_data = "" if aead_mode?
|
||||
|
||||
encrypted_data = cipher.update(Messages::Metadata.wrap(@serializer.dump(value), **metadata_options))
|
||||
encrypted_data = cipher.update(Messages::Metadata.wrap(serialize(value), **metadata_options))
|
||||
encrypted_data << cipher.final
|
||||
|
||||
encoded_encrypted_data = ::Base64.strict_encode64(encrypted_data)
|
||||
|
@ -214,8 +230,8 @@ module ActiveSupport
|
|||
decrypted_data << cipher.final
|
||||
|
||||
message = Messages::Metadata.verify(decrypted_data, purpose)
|
||||
@serializer.load(message) if message
|
||||
rescue OpenSSLCipherError, TypeError, ArgumentError
|
||||
deserialize(message) if message
|
||||
rescue OpenSSLCipherError, TypeError, ArgumentError, ::JSON::ParserError
|
||||
raise InvalidMessage
|
||||
end
|
||||
|
||||
|
|
|
@ -148,5 +148,32 @@ module ActiveSupport
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
initializer "active_support.set_fallback_to_marshal_deserialization" do |app|
|
||||
config.after_initialize do
|
||||
unless app.config.active_support.fallback_to_marshal_deserialization.nil?
|
||||
ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization =
|
||||
app.config.active_support.fallback_to_marshal_deserialization
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
initializer "active_support.set_default_message_encryptor_serializer" do |app|
|
||||
config.after_initialize do
|
||||
unless app.config.active_support.default_message_encryptor_serializer.nil?
|
||||
ActiveSupport::MessageEncryptor.default_message_encryptor_serializer =
|
||||
app.config.active_support.default_message_encryptor_serializer
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
initializer "active_support.set_marshal_serialization" do |app|
|
||||
config.after_initialize do
|
||||
unless app.config.active_support.use_marshal_serialization.nil?
|
||||
ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization =
|
||||
app.config.active_support.use_marshal_serialization
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -239,6 +239,137 @@ class MessageEncryptorTest < ActiveSupport::TestCase
|
|||
end
|
||||
end
|
||||
|
||||
class MessageEncryptorWithHybridSerializerAndMarshalDumpTest < MessageEncryptorTest
|
||||
def setup
|
||||
@secret = SecureRandom.random_bytes(32)
|
||||
@verifier = ActiveSupport::MessageVerifier.new(@secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
|
||||
@data = { some: "data", now: Time.local(2010) }
|
||||
@encryptor = ActiveSupport::MessageEncryptor.new(@secret)
|
||||
@default_message_encryptor_serializer = ActiveSupport::MessageEncryptor.default_message_encryptor_serializer
|
||||
@default_fallback_to_marshal_deserialization = ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization
|
||||
ActiveSupport::MessageEncryptor.default_message_encryptor_serializer = :hybrid
|
||||
end
|
||||
|
||||
def teardown
|
||||
ActiveSupport::MessageEncryptor.default_message_encryptor_serializer = @default_message_encryptor_serializer
|
||||
ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization = @default_fallback_to_marshal_deserialization
|
||||
super
|
||||
end
|
||||
|
||||
def test_backwards_compatibility_decrypt_previously_marshal_serialized_messages_when_fallback_to_marshal_deserialization_is_true
|
||||
secret = "\xB7\xF0\xBCW\xB1\x18`\xAB\xF0\x81\x10\xA4$\xF44\xEC\xA1\xDC\xC1\xDDD\xAF\xA9\xB8\x14\xCD\x18\x9A\x99 \x80)"
|
||||
data = "this_is_data"
|
||||
marshal_message = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal).encrypt_and_sign(data)
|
||||
assert_equal data, ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm").decrypt_and_verify(marshal_message)
|
||||
end
|
||||
|
||||
def test_failure_to_decrypt_marshal_serialized_messages_when_fallback_to_marshal_deserialization_is_false
|
||||
ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization = false
|
||||
secret = "\xB7\xF0\xBCW\xB1\x18`\xAB\xF0\x81\x10\xA4$\xF44\xEC\xA1\xDC\xC1\xDDD\xAF\xA9\xB8\x14\xCD\x18\x9A\x99 \x80)"
|
||||
data = "this_is_data"
|
||||
marshal_message = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm", serializer: Marshal).encrypt_and_sign(data)
|
||||
assert_raise(ActiveSupport::MessageEncryptor::InvalidMessage) do
|
||||
ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm").decrypt_and_verify(marshal_message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class MessageEncryptorWithJsonSerializerTest < MessageEncryptorTest
|
||||
def setup
|
||||
@secret = SecureRandom.random_bytes(32)
|
||||
@verifier = ActiveSupport::MessageVerifier.new(@secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
|
||||
@data = { "some" => "data", "now" => Time.local(2010) }
|
||||
@encryptor = ActiveSupport::MessageEncryptor.new(@secret)
|
||||
@default_message_encryptor_serializer = ActiveSupport::MessageEncryptor.default_message_encryptor_serializer
|
||||
ActiveSupport::MessageEncryptor.default_message_encryptor_serializer = :json
|
||||
end
|
||||
|
||||
def teardown
|
||||
ActiveSupport::MessageEncryptor.default_message_encryptor_serializer = @default_message_encryptor_serializer
|
||||
super
|
||||
end
|
||||
|
||||
def test_backwards_compat_for_64_bytes_key
|
||||
# 64 bit key
|
||||
secret = ["3942b1bf81e622559ed509e3ff274a780784fe9e75b065866bd270438c74da822219de3156473cc27df1fd590e4baf68c95eeb537b6e4d4c5a10f41635b5597e"].pack("H*")
|
||||
# Encryptor with 32 bit key, 64 bit secret for verifier
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(secret[0..31], secret)
|
||||
# Message generated with 64 bit key using the JSON serializer
|
||||
message = "YWZPOVBzcS8ycGZpVWpjWWU5NXZoanRtRGkzTVRjZk16UUpuRE9wa0pNNGI0dGxMRU5RdC94ZVhiQzUwYktFL2NEUjBnUm1QSlRDQ1RrMGlvakVZcGc9PS0tbXROMVdDUnNxWlRxMXA4dEtjWi9oZz09--3489b03e85aa14d8ed8cdd455507246f2e2884ae"
|
||||
assert_equal "data", encryptor.decrypt_and_verify(message)["some"]
|
||||
end
|
||||
|
||||
def test_backwards_compatibility_decrypt_previously_encrypted_messages_without_metadata
|
||||
secret = "\xB7\xF0\xBCW\xB1\x18`\xAB\xF0\x81\x10\xA4$\xF44\xEC\xA1\xDC\xC1\xDDD\xAF\xA9\xB8\x14\xCD\x18\x9A\x99 \x80)"
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: "aes-256-gcm")
|
||||
# Message generated with MessageEncryptor given JSON as the serializer.
|
||||
encrypted_message = "NIevxlF4gFUETmYTm3GJ--SIlwA5xxwpqNtiiv--woA9eLuVMbmapIDGDj7HQQ=="
|
||||
|
||||
assert_equal "Ruby on Rails", encryptor.decrypt_and_verify(encrypted_message)
|
||||
end
|
||||
|
||||
def test_on_rotation_is_called_and_returns_modified_messages
|
||||
older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign({ encoded: "message" })
|
||||
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(@secret)
|
||||
encryptor.rotate secrets[:old]
|
||||
encryptor.rotate secrets[:older], "older sign"
|
||||
|
||||
rotated = false
|
||||
message = encryptor.decrypt_and_verify(older_message, on_rotation: proc { rotated = true })
|
||||
|
||||
assert_equal({ "encoded" => "message" }, message)
|
||||
assert rotated
|
||||
end
|
||||
|
||||
def test_on_rotation_can_be_passed_at_the_constructor_level
|
||||
older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign({ encoded: "message" })
|
||||
|
||||
rotated = rotated = false # double assigning to suppress "assigned but unused variable" warning
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(@secret, on_rotation: proc { rotated = true })
|
||||
encryptor.rotate secrets[:older], "older sign"
|
||||
|
||||
assert_changes(:rotated, from: false, to: true) do
|
||||
message = encryptor.decrypt_and_verify(older_message)
|
||||
|
||||
assert_equal({ "encoded" => "message" }, message)
|
||||
end
|
||||
end
|
||||
|
||||
def test_on_rotation_option_takes_precedence_over_the_one_given_in_constructor
|
||||
older_message = ActiveSupport::MessageEncryptor.new(secrets[:older], "older sign").encrypt_and_sign({ encoded: "message" })
|
||||
|
||||
rotated = rotated = false # double assigning to suppress "assigned but unused variable" warning
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(@secret, on_rotation: proc { rotated = true })
|
||||
encryptor.rotate secrets[:older], "older sign"
|
||||
|
||||
assert_changes(:rotated, from: false, to: "Yes") do
|
||||
message = encryptor.decrypt_and_verify(older_message, on_rotation: proc { rotated = "Yes" })
|
||||
|
||||
assert_equal({ "encoded" => "message" }, message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
class MessageEncryptorWithHybridSerializerAndWithoutMarshalDumpTest < MessageEncryptorWithJsonSerializerTest
|
||||
def setup
|
||||
@secret = SecureRandom.random_bytes(32)
|
||||
@verifier = ActiveSupport::MessageVerifier.new(@secret, serializer: ActiveSupport::MessageEncryptor::NullSerializer)
|
||||
@data = { "some" => "data", "now" => Time.local(2010) }
|
||||
@encryptor = ActiveSupport::MessageEncryptor.new(@secret)
|
||||
@default_message_encryptor_serializer = ActiveSupport::MessageEncryptor.default_message_encryptor_serializer
|
||||
@default_use_marshal_serialization = ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization
|
||||
ActiveSupport::MessageEncryptor.default_message_encryptor_serializer = :hybrid
|
||||
ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization = false
|
||||
end
|
||||
|
||||
def teardown
|
||||
ActiveSupport::MessageEncryptor.default_message_encryptor_serializer = @default_message_encryptor_serializer
|
||||
ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization = @default_use_marshal_serialization
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
class MessageEncryptorMetadataTest < ActiveSupport::TestCase
|
||||
include SharedMessageMetadataTests
|
||||
|
||||
|
@ -272,3 +403,10 @@ class MessageEncryptorMetadataJSONTest < MessageEncryptorMetadataTest
|
|||
{ serializer: MessageEncryptorTest::JSONSerializer.new }
|
||||
end
|
||||
end
|
||||
|
||||
class MessageEncryptorMetadataJsonWithMarshalFallbackTest < MessageEncryptorMetadataTest
|
||||
private
|
||||
def encryptor_options
|
||||
{ serializer: ActiveSupport::JsonWithMarshalFallback }
|
||||
end
|
||||
end
|
||||
|
|
|
@ -62,6 +62,7 @@ Below are the default values associated with each target version. In cases of co
|
|||
|
||||
- [`config.action_dispatch.default_headers`](#config-action-dispatch-default-headers): `{ "X-Frame-Options" => "SAMEORIGIN", "X-XSS-Protection" => "0", "X-Content-Type-Options" => "nosniff", "X-Permitted-Cross-Domain-Policies" => "none", "Referrer-Policy" => "strict-origin-when-cross-origin" }`
|
||||
- [`config.add_autoload_paths_to_load_path`](#config-add-autoload-paths-to-load-path): `false`
|
||||
- [`config.active_support.default_message_encryptor_serializer`](#config-active-support-default-message-encryptor-serializer): `:json`
|
||||
|
||||
#### Default Values for Target Version 7.0
|
||||
|
||||
|
@ -1868,6 +1869,35 @@ The default value depends on the `config.load_defaults` target version:
|
|||
| (original) | `false` |
|
||||
| 6.1 | `true` |
|
||||
|
||||
#### `config.active_support.default_message_encryptor_serializer`
|
||||
|
||||
Specifies what serializer the `MessageEncryptor` class will use by default.
|
||||
|
||||
Options are `:json`, `:hybrid` and `:marshal`. `:hybrid` uses the `JsonWithMarshalFallback` class.
|
||||
|
||||
The default value depends on the `config.load_defaults` target version:
|
||||
|
||||
| Starting with version | The default value is |
|
||||
| --------------------- | -------------------- |
|
||||
| (original) | `:marshal` |
|
||||
| 7.1 | `:json` |
|
||||
|
||||
#### `config.active_support.fallback_to_marshal_deserialization`
|
||||
|
||||
Specifies if the `ActiveSupport::JsonWithMarshalFallback` class will fallback to `Marshal` when it encounters a `::JSON::ParserError`.
|
||||
|
||||
Defaults to `true`.
|
||||
|
||||
#### `config.active_support.use_marshal_serialization`
|
||||
|
||||
Specifies if the `ActiveSupport::JsonWithMarshalFallback` class will use `Marshal` to serialize payloads.
|
||||
|
||||
If this is set to false, it will use `JSON` to serialize payloads.
|
||||
|
||||
Used to help migrate apps from `Marshal` to `JSON` as the default serializer for the `MessageEncryptor` class.
|
||||
|
||||
Defaults to `true`.
|
||||
|
||||
### Configuring Active Job
|
||||
|
||||
`config.active_job` provides the following configuration options:
|
||||
|
|
|
@ -90,6 +90,85 @@ size of the `bootsnap` cache for the others.
|
|||
|
||||
Application controllers that inherit from `ActiveStorage::BaseController` and use streaming to implement custom file serving logic must now explicitly include the `ActiveStorage::Streaming` module.
|
||||
|
||||
### New `ActiveSupport::MessageEncryptor` default serializer
|
||||
|
||||
As of Rails 7.1, the default serializer in use by the `MessageEncryptor` is `JSON`.
|
||||
This offers a more secure alternative to the current default serializer.
|
||||
|
||||
The `MessageEncryptor` offers the ability to migrate the default serializer from `Marshal` to `JSON`.
|
||||
|
||||
If you would like to ignore this change in existing applications, set the following: `config.active_support.default_message_encryptor_serializer = :marshal`.
|
||||
|
||||
In order to roll out the new default when upgrading from `7.0` to `7.1`, there are three configuration variables to keep in mind.
|
||||
```
|
||||
config.active_support.default_message_encryptor_serializer
|
||||
config.active_support.fallback_to_marshal_deserialization
|
||||
config.active_support.use_marshal_serialization
|
||||
```
|
||||
|
||||
`default_message_encryptor_serializer` defaults to `:json` as of `7.1` but it offers both a `:hybrid` and `:marshal` option.
|
||||
|
||||
In order to migrate an older deployment to `:json`, first ensure that the `default_message_encryptor_serializer` is set to `:marshal`.
|
||||
```ruby
|
||||
# config/application.rb
|
||||
config.load_defaults 7.0
|
||||
config.active_support.default_message_encryptor_serializer = :marshal
|
||||
```
|
||||
|
||||
Once this is deployed on all Rails processes, set `default_message_encryptor_serializer` to `:hybrid` to begin using the
|
||||
`ActiveSupport::JsonWithMarshalFallback` class as the serializer. The defaults for this class are to use `Marshal`
|
||||
as the serializer and to allow the deserialisation of both `Marshal` and `JSON` serialized payloads.
|
||||
|
||||
```ruby
|
||||
config.load_defaults 7.0
|
||||
config.active_support.default_message_encryptor_serializer = :hybrid
|
||||
```
|
||||
|
||||
Once this is deployed on all Rails processes, set the following configuration options in order to stop the
|
||||
`ActiveSupport::JsonWithMarshalFallback` class from using `Marshal` to serialize new payloads.
|
||||
|
||||
```ruby
|
||||
# config/application.rb
|
||||
config.load_defaults 7.0
|
||||
config.active_support.default_message_encryptor_serializer = :hybrid
|
||||
config.active_support.use_marshal_serialization = false
|
||||
```
|
||||
|
||||
Allow this configuration to run on all processes for a considerable amount of time.
|
||||
`ActiveSupport::JsonWithMarshalFallback` logs the following each time the `Marshal` fallback
|
||||
is used:
|
||||
```
|
||||
JsonWithMarshalFallback: Marshal load fallback occurred.
|
||||
```
|
||||
|
||||
Once those message stop appearing in your logs and you're confident that all `MessageEncryptor`
|
||||
payloads in transit are `JSON` serialized, the following configuration options will disable the
|
||||
Marshal fallback in `ActiveSupport::JsonWithMarshalFallback`.
|
||||
|
||||
```ruby
|
||||
# config/application.rb
|
||||
config.load_defaults 7.0
|
||||
config.active_support.default_message_encryptor_serializer = :hybrid
|
||||
config.active_support.use_marshal_serialization = false
|
||||
config.active_support.fallback_to_marshal_deserialization = false
|
||||
```
|
||||
|
||||
If all goes well, you should now be safe to migrate the Message Encryptor from
|
||||
`ActiveSupport::JsonWithMarshalFallback` to `ActiveSupport::JSON`.
|
||||
To do so, simply swap the `:hybrid` serializer for the `:json` serializer.
|
||||
|
||||
```ruby
|
||||
# config/application.rb
|
||||
config.load_defaults 7.0
|
||||
config.active_support.default_message_encryptor_serializer = :json
|
||||
```
|
||||
|
||||
Alternatively, you could load defaults for 7.1
|
||||
```ruby
|
||||
# config/application.rb
|
||||
config.load_defaults 7.1
|
||||
```
|
||||
|
||||
Upgrading from Rails 6.1 to Rails 7.0
|
||||
-------------------------------------
|
||||
|
||||
|
|
|
@ -267,6 +267,10 @@ module Rails
|
|||
"Referrer-Policy" => "strict-origin-when-cross-origin"
|
||||
}
|
||||
end
|
||||
|
||||
if respond_to?(:active_support)
|
||||
active_support.default_message_encryptor_serializer = :json
|
||||
end
|
||||
else
|
||||
raise "Unknown version #{target_version.to_s.inspect}"
|
||||
end
|
||||
|
|
|
@ -3169,6 +3169,69 @@ module ApplicationTests
|
|||
assert_equal true, Rails.application.config.rake_eager_load
|
||||
end
|
||||
|
||||
test "ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization is true by default" do
|
||||
app "development"
|
||||
|
||||
assert_equal true, ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization
|
||||
end
|
||||
|
||||
test "ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization can be configured via config.active_support.fallback_to_marshal_deserialization" do
|
||||
remove_from_config '.*config\.load_defaults.*\n'
|
||||
|
||||
app_file "config/initializers/fallback_to_marshal_deserialization.rb", <<-RUBY
|
||||
Rails.application.config.active_support.fallback_to_marshal_deserialization = false
|
||||
RUBY
|
||||
|
||||
app "development"
|
||||
|
||||
assert_equal false, ActiveSupport::JsonWithMarshalFallback.fallback_to_marshal_deserialization
|
||||
end
|
||||
|
||||
test "ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization is true by default" do
|
||||
app "development"
|
||||
|
||||
assert_equal true, ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization
|
||||
end
|
||||
|
||||
test "ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization can be configured via config.active_support.use_marshal_serialization" do
|
||||
remove_from_config '.*config\.load_defaults.*\n'
|
||||
|
||||
app_file "config/initializers/use_marshal_serialization.rb", <<-RUBY
|
||||
Rails.application.config.active_support.use_marshal_serialization = false
|
||||
RUBY
|
||||
|
||||
app "development"
|
||||
|
||||
assert_equal false, ActiveSupport::JsonWithMarshalFallback.use_marshal_serialization
|
||||
end
|
||||
|
||||
test "ActiveSupport::MessageEncryptor.default_message_encryptor_serializer is :json by default" do
|
||||
app "development"
|
||||
|
||||
assert_equal :json, ActiveSupport::MessageEncryptor.default_message_encryptor_serializer
|
||||
end
|
||||
|
||||
test "ActiveSupport::MessageEncryptor.default_message_encryptor_serializer is :marshal by default for upgraded apps" do
|
||||
remove_from_config '.*config\.load_defaults.*\n'
|
||||
add_to_config 'config.load_defaults "6.1"'
|
||||
|
||||
app "development"
|
||||
|
||||
assert_equal :marshal, ActiveSupport::MessageEncryptor.default_message_encryptor_serializer
|
||||
end
|
||||
|
||||
test "ActiveSupport::MessageEncryptor.default_message_encryptor_serializer can be configured via config.active_support.default_message_encryptor_serializer" do
|
||||
remove_from_config '.*config\.load_defaults.*\n'
|
||||
|
||||
app_file "config/initializers/default_message_encryptor_serializer.rb", <<-RUBY
|
||||
Rails.application.config.active_support.default_message_encryptor_serializer = :hybrid
|
||||
RUBY
|
||||
|
||||
app "development"
|
||||
|
||||
assert_equal :hybrid, ActiveSupport::MessageEncryptor.default_message_encryptor_serializer
|
||||
end
|
||||
|
||||
test "unknown_asset_fallback is false by default" do
|
||||
app "development"
|
||||
|
||||
|
|
|
@ -158,8 +158,8 @@ module ApplicationTests
|
|||
second_secret = Rails.application.key_generator.generate_key("second", 32)
|
||||
|
||||
::TestEncryptors = Class.new do
|
||||
class_attribute :first_gcm, default: ActiveSupport::MessageEncryptor.new(first_secret, cipher: "aes-256-gcm")
|
||||
class_attribute :second_gcm, default: ActiveSupport::MessageEncryptor.new(second_secret, cipher: "aes-256-gcm")
|
||||
class_attribute :first_gcm, default: ActiveSupport::MessageEncryptor.new(first_secret, cipher: "aes-256-gcm", serializer: Marshal)
|
||||
class_attribute :second_gcm, default: ActiveSupport::MessageEncryptor.new(second_secret, cipher: "aes-256-gcm", serializer: Marshal)
|
||||
end
|
||||
|
||||
config.action_dispatch.use_authenticated_cookie_encryption = true
|
||||
|
@ -175,6 +175,77 @@ module ApplicationTests
|
|||
|
||||
require "#{app_path}/config/environment"
|
||||
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(app.key_generator.generate_key("salt", 32), cipher: "aes-256-gcm", serializer: Marshal)
|
||||
|
||||
get "/foo/write_raw_cookie_one"
|
||||
get "/foo/read_encrypted"
|
||||
assert_equal "encrypted cookie".inspect, last_response.body
|
||||
|
||||
get "/foo/read_raw_cookie"
|
||||
assert_equal "encrypted cookie", encryptor.decrypt_and_verify(last_response.body, purpose: "cookie.encrypted_cookie")
|
||||
|
||||
get "/foo/write_raw_cookie_two"
|
||||
get "/foo/read_encrypted"
|
||||
assert_equal "encrypted cookie".inspect, last_response.body
|
||||
|
||||
get "/foo/read_raw_cookie"
|
||||
assert_equal "encrypted cookie", encryptor.decrypt_and_verify(last_response.body, purpose: "cookie.encrypted_cookie")
|
||||
end
|
||||
|
||||
test "encrypted cookies rotating multiple encryption keys with cookies serializer as json" do
|
||||
app_file "config/routes.rb", <<-RUBY
|
||||
Rails.application.routes.draw do
|
||||
get ':controller(/:action)'
|
||||
post ':controller(/:action)'
|
||||
end
|
||||
RUBY
|
||||
|
||||
controller :foo, <<-RUBY
|
||||
class FooController < ActionController::Base
|
||||
protect_from_forgery with: :null_session
|
||||
|
||||
def write_raw_cookie_one
|
||||
cookies[:encrypted_cookie] = TestEncryptors.first_gcm.encrypt_and_sign("encrypted cookie")
|
||||
head :ok
|
||||
end
|
||||
|
||||
def write_raw_cookie_two
|
||||
cookies[:encrypted_cookie] = TestEncryptors.second_gcm.encrypt_and_sign("encrypted cookie")
|
||||
head :ok
|
||||
end
|
||||
|
||||
def read_encrypted
|
||||
render plain: cookies.encrypted[:encrypted_cookie].inspect
|
||||
end
|
||||
|
||||
def read_raw_cookie
|
||||
render plain: cookies[:encrypted_cookie]
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
|
||||
add_to_config <<-RUBY
|
||||
first_secret = Rails.application.key_generator.generate_key("first", 32)
|
||||
second_secret = Rails.application.key_generator.generate_key("second", 32)
|
||||
|
||||
::TestEncryptors = Class.new do
|
||||
class_attribute :first_gcm, default: ActiveSupport::MessageEncryptor.new(first_secret, cipher: "aes-256-gcm", serializer: ActiveSupport::JSON)
|
||||
class_attribute :second_gcm, default: ActiveSupport::MessageEncryptor.new(second_secret, cipher: "aes-256-gcm", serializer: ActiveSupport::JSON)
|
||||
end
|
||||
|
||||
config.action_dispatch.use_authenticated_cookie_encryption = true
|
||||
config.action_dispatch.encrypted_cookie_cipher = "aes-256-gcm"
|
||||
config.action_dispatch.authenticated_encrypted_cookie_salt = "salt"
|
||||
config.action_dispatch.cookies_serializer = :json
|
||||
|
||||
config.action_dispatch.cookies_rotations.tap do |cookies|
|
||||
cookies.rotate :encrypted, first_secret
|
||||
cookies.rotate :encrypted, second_secret
|
||||
end
|
||||
RUBY
|
||||
|
||||
require "#{app_path}/config/environment"
|
||||
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(app.key_generator.generate_key("salt", 32), cipher: "aes-256-gcm")
|
||||
|
||||
get "/foo/write_raw_cookie_one"
|
||||
|
|
|
@ -136,7 +136,59 @@ module ApplicationTests
|
|||
assert_equal '"1"', last_response.body
|
||||
end
|
||||
|
||||
test "session using encrypted cookie store" do
|
||||
test "session using encrypted cookie store with json serializer" do
|
||||
app_file "config/routes.rb", <<-RUBY
|
||||
Rails.application.routes.draw do
|
||||
get ':controller(/:action)'
|
||||
end
|
||||
RUBY
|
||||
|
||||
controller :foo, <<-RUBY
|
||||
class FooController < ActionController::Base
|
||||
def write_session
|
||||
session[:foo] = 1
|
||||
head :ok
|
||||
end
|
||||
|
||||
def read_session
|
||||
render plain: session[:foo]
|
||||
end
|
||||
|
||||
def read_encrypted_cookie
|
||||
render plain: cookies.encrypted[:_myapp_session]['foo']
|
||||
end
|
||||
|
||||
def read_raw_cookie
|
||||
render plain: cookies[:_myapp_session]
|
||||
end
|
||||
end
|
||||
RUBY
|
||||
|
||||
add_to_config <<-RUBY
|
||||
# Enable AEAD cookies
|
||||
config.action_dispatch.use_authenticated_cookie_encryption = true
|
||||
|
||||
config.action_dispatch.cookies_serializer = :json
|
||||
RUBY
|
||||
|
||||
require "#{app_path}/config/environment"
|
||||
|
||||
get "/foo/write_session"
|
||||
get "/foo/read_session"
|
||||
assert_equal "1", last_response.body
|
||||
|
||||
get "/foo/read_encrypted_cookie"
|
||||
assert_equal "1", last_response.body
|
||||
|
||||
cipher = "aes-256-gcm"
|
||||
secret = app.key_generator.generate_key("authenticated encrypted cookie")
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
|
||||
|
||||
get "/foo/read_raw_cookie"
|
||||
assert_equal 1, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"]
|
||||
end
|
||||
|
||||
test "session using encrypted cookie store with marshal serializer" do
|
||||
app_file "config/routes.rb", <<-RUBY
|
||||
Rails.application.routes.draw do
|
||||
get ':controller(/:action)'
|
||||
|
@ -182,7 +234,7 @@ module ApplicationTests
|
|||
|
||||
cipher = "aes-256-gcm"
|
||||
secret = app.key_generator.generate_key("authenticated encrypted cookie")
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher, serializer: Marshal)
|
||||
|
||||
get "/foo/read_raw_cookie"
|
||||
assert_equal 1, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"]
|
||||
|
@ -233,7 +285,7 @@ module ApplicationTests
|
|||
|
||||
cipher = "aes-256-gcm"
|
||||
secret = app.key_generator.generate_key("authenticated encrypted cookie")
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher, serializer: Marshal)
|
||||
|
||||
get "/foo/read_raw_cookie"
|
||||
assert_equal 1, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"]
|
||||
|
@ -305,7 +357,7 @@ module ApplicationTests
|
|||
|
||||
cipher = "aes-256-gcm"
|
||||
secret = app.key_generator.generate_key("authenticated encrypted cookie")
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher)
|
||||
encryptor = ActiveSupport::MessageEncryptor.new(secret[0, ActiveSupport::MessageEncryptor.key_len(cipher)], cipher: cipher, serializer: Marshal)
|
||||
|
||||
get "/foo/read_raw_cookie"
|
||||
assert_equal 2, encryptor.decrypt_and_verify(last_response.body, purpose: "cookie._myapp_session")["foo"]
|
||||
|
|
|
@ -39,6 +39,13 @@ class Rails::Command::SecretsCommandTest < ActiveSupport::TestCase
|
|||
end
|
||||
|
||||
test "edit secrets" do
|
||||
# Use expected default MessageEncryptor serializer for Rails < 7.1 to be compatible with hardcoded secrets.yml.enc
|
||||
add_to_config <<-RUBY
|
||||
config.active_support.default_message_encryptor_serializer = :marshal
|
||||
RUBY
|
||||
|
||||
require "#{app_path}/config/environment"
|
||||
|
||||
prevent_deprecation
|
||||
|
||||
# Run twice to ensure encrypted secrets can be reread after first edit pass.
|
||||
|
|
Loading…
Reference in a new issue