Skip to content

Commit

Permalink
Unify Message{Encryptor,Verifier} serializer config
Browse files Browse the repository at this point in the history
In #42843 and #42846, several config settings were added to control the
default serializer for `MessageEncryptor` and `MessageVerifier`, and to
provide a migration path from a default `Marshal` serializer to a
default `JSON` serializer:

* `config.active_support.default_message_encryptor_serializer`
  * Supports `:marshal`, `:hybrid`, or `:json`.
* `config.active_support.default_message_verifier_serializer`
  * Supports `:marshal`, `:hybrid`, or `:json`.
* `config.active_support.fallback_to_marshal_deserialization`
  * Affects `:hybrid` for both `MessageEncryptor` and `MessageVerifier`.
* `config.active_support.use_marshal_serialization`
  * Affects `:hybrid` for both `MessageEncryptor` and `MessageVerifier`.

This commit unifies those config settings into a single setting,
`config.active_support.message_serializer`, which supports `:marshal`,
`:json_allow_marshal`, and `:json` values.  So, for example,

  ```ruby
  config.active_support.default_message_encryptor_serializer = :hybrid
  config.active_support.default_message_verifier_serializer = :hybrid
  config.active_support.fallback_to_marshal_deserialization = true
  config.active_support.use_marshal_serialization = false
  ```

becomes

  ```ruby
  config.active_support.message_serializer = :json_allow_marshal
  ```

and

  ```ruby
  config.active_support.default_message_encryptor_serializer = :hybrid
  config.active_support.default_message_verifier_serializer = :hybrid
  config.active_support.fallback_to_marshal_deserialization = false
  config.active_support.use_marshal_serialization = false
  ```

becomes

  ```ruby
  config.active_support.message_serializer = :json
  ```

This commit also replaces `ActiveSupport::JsonWithMarshalFallback` with
`ActiveSupport::Messages::SerializerWithFallback`, which implements a
generic mechanism for serializer fallback.  The `:marshal` serializer
uses this mechanism too, so

  ```ruby
  config.active_support.default_message_encryptor_serializer = :hybrid
  config.active_support.default_message_verifier_serializer = :hybrid
  config.active_support.fallback_to_marshal_deserialization = false
  config.active_support.use_marshal_serialization = true
  ```

becomes

  ```ruby
  config.active_support.message_serializer = :marshal
  ```

Additionally, the logging behavior of `JsonWithMarshalFallback` has been
replaced with notifications which include the names of the intended and
actual serializers, as well as the serialized and deserialized message
data.  This provides a more targeted means of tracking serializer
fallback events.  It also allows the user to "silence" such events, if
desired, without an additional config setting.

All of these changes make it easier to add migration paths for new
serializers such as `ActiveSupport::MessagePack`.
  • Loading branch information
jonathanhefner committed May 8, 2023
1 parent 016b796 commit 9fbfd81
Show file tree
Hide file tree
Showing 22 changed files with 411 additions and 675 deletions.
14 changes: 12 additions & 2 deletions activesupport/CHANGELOG.md
Expand Up @@ -753,9 +753,19 @@

*John Hawthorn*

* Change default serialization format of `MessageEncryptor` from `Marshal` to `JSON` for Rails 7.1.
* Change the default serializer of `ActiveSupport::MessageVerifier` from
`Marshal` to `ActiveSupport::JSON` when using `config.load_defaults 7.1`.

Existing apps are provided with an upgrade path to migrate to `JSON` as described in `guides/source/upgrading_ruby_on_rails.md`
Existing apps can use the `:json_allow_marshal` serializer to migrate. See
https://guides.rubyonrails.org/v7.1/configuring.html#config-active-support-message-serializer.

*Saba Kiaei* and *David Buckley*

* Change the default serializer of `ActiveSupport::MessageEncryptor` from
`Marshal` to `ActiveSupport::JSON` when using `config.load_defaults 7.1`.

Existing apps can use the `:json_allow_marshal` serializer to migrate. See
https://guides.rubyonrails.org/v7.1/configuring.html#config-active-support-message-serializer.

*Zack Deveau* and *Martin Gingras*

Expand Down
1 change: 0 additions & 1 deletion activesupport/lib/active_support.rb
Expand Up @@ -67,7 +67,6 @@ module ActiveSupport
autoload :Gzip
autoload :Inflector
autoload :JSON
autoload :JsonWithMarshalFallback
autoload :KeyGenerator
autoload :MessageEncryptor
autoload :MessageEncryptors
Expand Down
42 changes: 0 additions & 42 deletions activesupport/lib/active_support/json_with_marshal_fallback.rb

This file was deleted.

35 changes: 22 additions & 13 deletions activesupport/lib/active_support/message_encryptor.rb
Expand Up @@ -91,7 +91,6 @@ class MessageEncryptor < Messages::Codec
prepend Messages::Rotator

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:
Expand Down Expand Up @@ -142,10 +141,24 @@ class InvalidMessage < StandardError; end
# 'aes-256-gcm'.
#
# [+:serializer+]
# By default, MessageEncryptor uses JSON to serialize the message. If
# you want to use another serialization method, you can pass an object
# that responds to +load+ and +dump+, like +Marshal+ or +YAML+.
# +:marshal+, +:hybrid+, and +:json+ are also supported.
# The serializer used to serialize message data. You can specify any
# object that responds to +dump+ and +load+, or you can choose from
# several preconfigured serializers: +:marshal+, +:json_allow_marshal+,
# +:json+.
#
# The preconfigured serializers include a fallback mechanism to support
# multiple deserialization formats. For example, the +:marshal+ serializer
# will serialize using +Marshal+, but can deserialize using +Marshal+ or
# ActiveSupport::JSON. This makes it easy to migrate between serializers.
#
# The +:marshal+ and +:json_allow_marshal+ serializers support
# deserializing using +Marshal+, but :+json+ does not. Beware that
# +Marshal+ is a potential vector for deserialization attacks in cases
# where a message signing secret has been leaked. <em>If possible, choose
# a serializer that does not support +Marshal+.</em>
#
# When using \Rails with <tt>config.load_defaults 7.1</tt> or later, the
# default is +:json+. Otherwise, the default is +:marshal+.
#
# [+:url_safe+]
# By default, MessageEncryptor generates RFC 4648 compliant strings
Expand All @@ -161,17 +174,13 @@ class InvalidMessage < StandardError; end
#
# If you don't pass a truthy value, the default is set using
# +config.active_support.use_message_serializer_for_metadata+.
def initialize(secret, sign_secret = nil, cipher: nil, digest: nil, serializer: nil, url_safe: false, force_legacy_metadata_serializer: false)
super(
serializer: serializer || @@default_message_encryptor_serializer,
url_safe: url_safe,
force_legacy_metadata_serializer: force_legacy_metadata_serializer,
)
def initialize(secret, sign_secret = nil, **options)
super(**options)
@secret = secret
@cipher = cipher || self.class.default_cipher
@cipher = options[:cipher] || self.class.default_cipher
@aead_mode = new_cipher.authenticated?
@verifier = if !@aead_mode
MessageVerifier.new(sign_secret || secret, digest: digest || "SHA1", serializer: NullSerializer, url_safe: url_safe)
MessageVerifier.new(sign_secret || secret, **options, serializer: NullSerializer)
end
end

Expand Down
34 changes: 21 additions & 13 deletions activesupport/lib/active_support/message_verifier.rb
Expand Up @@ -103,8 +103,6 @@ class InvalidSignature < StandardError; end
SEPARATOR = "--" # :nodoc:
SEPARATOR_LENGTH = SEPARATOR.length # :nodoc:

cattr_accessor :default_message_verifier_serializer, instance_accessor: false, default: :marshal

# Initialize a new MessageVerifier with a secret for the signature.
#
# ==== Options
Expand All @@ -114,10 +112,24 @@ class InvalidSignature < StandardError; end
# +OpenSSL::Digest+ for alternatives.
#
# [+:serializer+]
# By default, MessageVerifier uses JSON to serialize the message. If you want
# to use another serialization method, you can pass an object that responds
# to +load+ and +dump+, like +Marshal+ or +YAML+.
# +:marshal+, +:hybrid+, and +:json+ are also supported.
# The serializer used to serialize message data. You can specify any
# object that responds to +dump+ and +load+, or you can choose from
# several preconfigured serializers: +:marshal+, +:json_allow_marshal+,
# +:json+.
#
# The preconfigured serializers include a fallback mechanism to support
# multiple deserialization formats. For example, the +:marshal+ serializer
# will serialize using +Marshal+, but can deserialize using +Marshal+ or
# ActiveSupport::JSON. This makes it easy to migrate between serializers.
#
# The +:marshal+ and +:json_allow_marshal+ serializers support
# deserializing using +Marshal+, but :+json+ does not. Beware that
# +Marshal+ is a potential vector for deserialization attacks in cases
# where a message signing secret has been leaked. <em>If possible, choose
# a serializer that does not support +Marshal+.</em>
#
# When using \Rails with <tt>config.load_defaults 7.1</tt> or later, the
# default is +:json+. Otherwise, the default is +:marshal+.
#
# [+:url_safe+]
# By default, MessageVerifier generates RFC 4648 compliant strings which are
Expand All @@ -132,15 +144,11 @@ class InvalidSignature < StandardError; end
#
# If you don't pass a truthy value, the default is set using
# +config.active_support.use_message_serializer_for_metadata+.
def initialize(secret, digest: nil, serializer: nil, url_safe: false, force_legacy_metadata_serializer: false)
def initialize(secret, **options)
raise ArgumentError, "Secret should not be nil." unless secret
super(
serializer: serializer || @@default_message_verifier_serializer,
url_safe: url_safe,
force_legacy_metadata_serializer: force_legacy_metadata_serializer,
)
super(**options)
@secret = secret
@digest = digest&.to_s || "SHA1"
@digest = options[:digest]&.to_s || "SHA1"
end

# Checks if a signed message could have been generated by signing an object
Expand Down
25 changes: 10 additions & 15 deletions activesupport/lib/active_support/messages/codec.rb
@@ -1,27 +1,22 @@
# frozen_string_literal: true

require "active_support/messages/metadata"
require "active_support/core_ext/class/attribute"
require_relative "metadata"
require_relative "serializer_with_fallback"

module ActiveSupport
module Messages # :nodoc:
class Codec # :nodoc:
include Metadata

def initialize(serializer:, url_safe:, force_legacy_metadata_serializer: false)
@serializer =
case serializer
when :marshal
Marshal
when :hybrid
JsonWithMarshalFallback
when :json
JSON
else
serializer
end
class_attribute :default_serializer, default: :marshal,
instance_accessor: false, instance_predicate: false

@url_safe = url_safe
@force_legacy_metadata_serializer = force_legacy_metadata_serializer
def initialize(**options)
@serializer = options[:serializer] || self.class.default_serializer
@serializer = SerializerWithFallback[@serializer] if @serializer.is_a?(Symbol)
@url_safe = options[:url_safe]
@force_legacy_metadata_serializer = options[:force_legacy_metadata_serializer]
end

private
Expand Down
5 changes: 3 additions & 2 deletions activesupport/lib/active_support/messages/metadata.rb
Expand Up @@ -2,16 +2,17 @@

require "time"
require "active_support/json"
require_relative "serializer_with_fallback"

module ActiveSupport
module Messages # :nodoc:
module Metadata # :nodoc:
singleton_class.attr_accessor :use_message_serializer_for_metadata

ENVELOPE_SERIALIZERS = [
::JSON,
*SerializerWithFallback::SERIALIZERS.values,
ActiveSupport::JSON,
ActiveSupport::JsonWithMarshalFallback,
::JSON,
Marshal,
]

Expand Down
113 changes: 113 additions & 0 deletions activesupport/lib/active_support/messages/serializer_with_fallback.rb
@@ -0,0 +1,113 @@
# frozen_string_literal: true

require "active_support/notifications"

module ActiveSupport
module Messages # :nodoc:
module SerializerWithFallback # :nodoc:
def self.[](format)
SERIALIZERS.fetch(format)
end

def load(dumped)
format = detect_format(dumped)

if format == self.format
_load(dumped)
elsif format && fallback?(format)
payload = { serializer: SERIALIZERS.key(self), fallback: format, serialized: dumped }
ActiveSupport::Notifications.instrument("message_serializer_fallback.active_support", payload) do
payload[:deserialized] = SERIALIZERS[format]._load(dumped)
end
else
raise "Unsupported serialization format"
end
end

private
def detect_format(dumped)
case
when MarshalWithFallback.dumped?(dumped)
:marshal
when JsonWithFallback.dumped?(dumped)
:json
end
end

def fallback?(format)
format != :marshal
end

module AllowMarshal
private
def fallback?(format)
super || format == :marshal
end
end

module MarshalWithFallback
include SerializerWithFallback
extend self

def format
:marshal
end

def dump(object)
Marshal.dump(object)
end

def _load(dumped)
Marshal.load(dumped)
end

MARSHAL_SIGNATURE = "\x04\x08"

def dumped?(dumped)
dumped.start_with?(MARSHAL_SIGNATURE)
end
end

module JsonWithFallback
include SerializerWithFallback
extend self

def format
:json
end

def dump(object)
ActiveSupport::JSON.encode(object)
end

def _load(dumped)
ActiveSupport::JSON.decode(dumped)
end

JSON_START_WITH = /\A(?:[{\["]|-?\d|true|false|null)/

def dumped?(dumped)
JSON_START_WITH.match?(dumped)
end

private
def detect_format(dumped)
# Assume JSON format if format could not be determined.
super || :json
end
end

module JsonWithFallbackAllowMarshal
include JsonWithFallback
include AllowMarshal
extend self
end

SERIALIZERS = {
marshal: MarshalWithFallback,
json: JsonWithFallback,
json_allow_marshal: JsonWithFallbackAllowMarshal,
}
end
end
end

0 comments on commit 9fbfd81

Please sign in to comment.