From d28f278788b599c0a9f6e3ea437c6642eb56f16c Mon Sep 17 00:00:00 2001
From: Zack <zack.ref@gmail.com>
Date: Fri, 22 Apr 2022 15:21:40 -0400
Subject: [PATCH] Change ActiveRecord::Coders::YAMLColumn default to safe_load

In Psych >= 4.0.0, load defaults to safe_load. This commit
makes the ActiveRecord::Coders::YAMLColum class use Psych safe_load
as the Rails default.

This default is configurable via ActiveRecord::Base.use_yaml_unsafe_load

We conditionally fallback to the correct unsafe load if use_yaml_unsafe_load
is set to true. unsafe_load was introduced in Psych >= 4.0.0

The list of safe_load permitted classes is configurable via
ActiveRecord::Base.yaml_column_permitted_classes

[CVE-2022-32224]
---
 .../lib/active_record/coders/yaml_column.rb   | 14 +++-
 activerecord/lib/active_record/core.rb        | 10 +++
 activerecord/lib/active_record/railtie.rb     | 18 +++++
 .../test/cases/attribute_methods_test.rb      |  6 +-
 activerecord/test/cases/base_test.rb          |  2 +-
 activerecord/test/cases/calculations_test.rb  |  4 +-
 .../test/cases/coders/yaml_column_test.rb     | 34 ++++++++
 activerecord/test/cases/dirty_test.rb         | 20 ++---
 .../test/cases/filter_attributes_test.rb      |  1 +
 .../test/cases/json_serialization_test.rb     |  2 +-
 activerecord/test/cases/serialization_test.rb |  2 +-
 .../test/cases/serialized_attribute_test.rb   | 77 +++++++++++++++++++
 activerecord/test/cases/store_test.rb         | 22 +++++-
 .../test/cases/yaml_serialization_test.rb     |  8 +-
 activerecord/test/models/admin/user_json.rb   | 48 ++++++++++++
 activerecord/test/schema/schema.rb            | 15 ++++
 .../rails_4_1_no_symbol.yml                   | 22 ++++++
 .../test/application/configuration_test.rb    | 33 ++++++++
 18 files changed, 314 insertions(+), 24 deletions(-)
 create mode 100644 activerecord/test/models/admin/user_json.rb
 create mode 100644 activerecord/test/support/yaml_compatibility_fixtures/rails_4_1_no_symbol.yml

diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb
index 881f0bcdb0..4a221662e0 100644
--- a/activerecord/lib/active_record/coders/yaml_column.rb
+++ b/activerecord/lib/active_record/coders/yaml_column.rb
@@ -23,7 +23,7 @@ def dump(obj)
       def load(yaml)
         return object_class.new if object_class != Object && yaml.nil?
         return yaml unless yaml.is_a?(String) && /^---/.match?(yaml)
-        obj = YAML.load(yaml)
+        obj = yaml_load(yaml)
 
         assert_valid_value(obj, action: "load")
         obj ||= object_class.new if object_class != Object
@@ -44,6 +44,18 @@ def check_arity_of_constructor
         rescue ArgumentError
           raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor."
         end
+
+        def yaml_load(payload)
+          if !ActiveRecord::Base.use_yaml_unsafe_load
+            YAML.safe_load(payload, permitted_classes: ActiveRecord::Base.yaml_column_permitted_classes, aliases: true)
+          else
+            if YAML.respond_to?(:unsafe_load)
+              YAML.unsafe_load(payload)
+            else
+              YAML.load(payload)
+            end
+          end
+        end
     end
   end
 end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index d5a2dbc34f..e3cc48e47f 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -128,6 +128,16 @@ def self.configurations
 
       mattr_accessor :reading_role, instance_accessor: false, default: :reading
 
+      ##
+      # :singleton-method:
+      # Application configurable boolean that instructs the YAML Coder to use
+      # an unsafe load if set to true.
+      mattr_accessor :use_yaml_unsafe_load, instance_writer: false, default: false
+
+      # Application configurable array that provides additional permitted classes
+      # to Psych safe_load in the YAML Coder
+      mattr_accessor :yaml_column_permitted_classes, instance_writer: false, default: []
+
       class_attribute :default_connection_handler, instance_writer: false
 
       self.filter_attributes = []
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index a5171cda05..ce267a9e11 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -259,5 +259,23 @@ class Railtie < Rails::Railtie # :nodoc:
         self.filter_attributes += Rails.application.config.filter_parameters
       end
     end
+
+    initializer "active_record.use_yaml_unsafe_load" do |app|
+      config.after_initialize do
+        unless app.config.active_record.use_yaml_unsafe_load.nil?
+          ActiveRecord::Base.use_yaml_unsafe_load =
+            app.config.active_record.use_yaml_unsafe_load
+        end
+      end
+    end
+
+    initializer "active_record.yaml_column_permitted_classes" do |app|
+      config.after_initialize do
+        unless app.config.active_record.yaml_column_permitted_classes.nil?
+          ActiveRecord::Base.yaml_column_permitted_classes =
+            app.config.active_record.yaml_column_permitted_classes
+        end
+      end
+    end
   end
 end
diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb
index 71b5407dcc..b56ed65c54 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -44,9 +44,9 @@ def setup
 
   test "attribute_for_inspect with an array" do
     t = topics(:first)
-    t.content = [Object.new]
+    t.content = ["some_value"]
 
-    assert_match %r(\[#<Object:0x[0-9a-f]+>\]), t.attribute_for_inspect(:content)
+    assert_match %r(\[\"some_value\"\]), t.attribute_for_inspect(:content)
   end
 
   test "attribute_for_inspect with a long array" do
@@ -600,7 +600,7 @@ def topic.title() "b" end
   end
 
   test "should unserialize attributes for frozen records" do
-    myobj = { value1: :value2 }
+    myobj = { "value1" => "value2" }
     topic = Topic.create(content: myobj)
     topic.freeze
     assert_equal myobj, topic.content
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 75cfe2d749..3d2ae13211 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -79,7 +79,7 @@ def test_generated_relation_methods_module_name
 
   def test_incomplete_schema_loading
     topic = Topic.first
-    payload = { foo: 42 }
+    payload = { "foo" => 42 }
     topic.update!(content: payload)
 
     Topic.reset_column_information
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 825e23ec23..9ad08958a2 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -757,8 +757,8 @@ def test_pluck_on_aliased_attribute
   end
 
   def test_pluck_with_serialization
-    t = Topic.create!(content: { foo: :bar })
-    assert_equal [{ foo: :bar }], Topic.where(id: t.id).pluck(:content)
+    t = Topic.create!(content: { "foo" => "bar" })
+    assert_equal [{ "foo" => "bar" }], Topic.where(id: t.id).pluck(:content)
   end
 
   def test_pluck_with_qualified_column_name
diff --git a/activerecord/test/cases/coders/yaml_column_test.rb b/activerecord/test/cases/coders/yaml_column_test.rb
index 4a5559c62f..01f31d2acd 100644
--- a/activerecord/test/cases/coders/yaml_column_test.rb
+++ b/activerecord/test/cases/coders/yaml_column_test.rb
@@ -5,6 +5,10 @@
 module ActiveRecord
   module Coders
     class YAMLColumnTest < ActiveRecord::TestCase
+      setup do
+        ActiveRecord::Base.use_yaml_unsafe_load = true
+      end
+      
       def test_initialize_takes_class
         coder = YAMLColumn.new("attr_name", Object)
         assert_equal Object, coder.object_class
@@ -62,5 +66,35 @@ def test_load_doesnt_handle_undefined_class_or_module
         end
       end
     end
+
+    class YAMLColumnTestWithSafeLoad < YAMLColumnTest
+      setup do
+         @yaml_column_permitted_classes_default = ActiveRecord::Base.yaml_column_permitted_classes
+        ActiveRecord::Base.use_yaml_unsafe_load = false
+      end
+
+      def test_yaml_column_permitted_classes_are_consumed_by_safe_load
+        ActiveRecord::Base.yaml_column_permitted_classes = [Symbol, Time]
+
+        coder = YAMLColumn.new("attr_name")
+        time_yaml = YAML.dump(Time.new)
+        symbol_yaml = YAML.dump(:somesymbol)
+
+        assert_nothing_raised do
+          coder.load(time_yaml)
+          coder.load(symbol_yaml)
+        end
+
+        ActiveRecord::Base.yaml_column_permitted_classes = @yaml_column_permitted_classes_default
+      end
+      
+      def test_load_doesnt_handle_undefined_class_or_module
+        coder = YAMLColumn.new("attr_name")
+        missing_class_yaml = '--- !ruby/object:DoesNotExistAndShouldntEver {}\n'
+        assert_raises(Psych::DisallowedClass) do
+          coder.load(missing_class_yaml)
+        end
+      end
+    end
   end
 end
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index 5566b372cc..f0c51465ed 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -433,44 +433,44 @@ def test_reverted_changes_are_not_dirty_going_from_nil_to_value_and_back
 
   def test_save_should_store_serialized_attributes_even_with_partial_writes
     with_partial_writes(Topic) do
-      topic = Topic.create!(content: { a: "a" })
+      topic = Topic.create!(content: { "a" => "a" })
 
       assert_not_predicate topic, :changed?
 
-      topic.content[:b] = "b"
+      topic.content["b"] = "b"
 
       assert_predicate topic, :changed?
 
       topic.save!
 
       assert_not_predicate topic, :changed?
-      assert_equal "b", topic.content[:b]
+      assert_equal "b", topic.content["b"]
 
       topic.reload
 
-      assert_equal "b", topic.content[:b]
+      assert_equal "b", topic.content["b"]
     end
   end
 
   def test_save_always_should_update_timestamps_when_serialized_attributes_are_present
     with_partial_writes(Topic) do
-      topic = Topic.create!(content: { a: "a" })
+      topic = Topic.create!(content: { "a" => "a" })
       topic.save!
 
       updated_at = topic.updated_at
       travel(1.second) do
-        topic.content[:hello] = "world"
+        topic.content["hello"] = "world"
         topic.save!
       end
 
       assert_not_equal updated_at, topic.updated_at
-      assert_equal "world", topic.content[:hello]
+      assert_equal "world", topic.content["hello"]
     end
   end
 
   def test_save_should_not_save_serialized_attribute_with_partial_writes_if_not_present
     with_partial_writes(Topic) do
-      topic = Topic.create!(author_name: "Bill", content: { a: "a" })
+      topic = Topic.create!(author_name: "Bill", content: { "a" => "a" })
       topic = Topic.select("id, author_name").find(topic.id)
       topic.update_columns author_name: "John"
       assert_not_nil topic.reload.content
@@ -478,11 +478,11 @@ def test_save_should_not_save_serialized_attribute_with_partial_writes_if_not_pr
   end
 
   def test_changes_to_save_should_not_mutate_array_of_hashes
-    topic = Topic.new(author_name: "Bill", content: [{ a: "a" }])
+    topic = Topic.new(author_name: "Bill", content: [{ "a" => "a" }])
 
     topic.changes_to_save
 
-    assert_equal [{ a: "a" }], topic.content
+    assert_equal [{ "a" => "a" }], topic.content
   end
 
   def test_previous_changes
diff --git a/activerecord/test/cases/filter_attributes_test.rb b/activerecord/test/cases/filter_attributes_test.rb
index 0ace90d523..aa44fd71d6 100644
--- a/activerecord/test/cases/filter_attributes_test.rb
+++ b/activerecord/test/cases/filter_attributes_test.rb
@@ -13,6 +13,7 @@ class FilterAttributesTest < ActiveRecord::TestCase
   setup do
     @previous_filter_attributes = ActiveRecord::Base.filter_attributes
     ActiveRecord::Base.filter_attributes = [:name]
+    ActiveRecord::Base.use_yaml_unsafe_load = true
   end
 
   teardown do
diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb
index 31691d83fc..0f39858eec 100644
--- a/activerecord/test/cases/json_serialization_test.rb
+++ b/activerecord/test/cases/json_serialization_test.rb
@@ -33,7 +33,7 @@ def setup
       avatar: "binarydata",
       created_at: Time.utc(2006, 8, 1),
       awesome: true,
-      preferences: { shows: "anime" }
+      preferences: { "shows" => "anime" }
     )
   end
 
diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb
index 932780bfef..65c6e3204c 100644
--- a/activerecord/test/cases/serialization_test.rb
+++ b/activerecord/test/cases/serialization_test.rb
@@ -19,7 +19,7 @@ def setup
       avatar: "binarydata",
       created_at: Time.utc(2006, 8, 1),
       awesome: false,
-      preferences: { gem: "<strong>ruby</strong>" },
+      preferences: { "gem" => "<strong>ruby</strong>" },
       alternative_id: nil,
       id: nil
     }
diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb
index fb2f3ca2b3..1842841796 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -8,6 +8,10 @@
 class SerializedAttributeTest < ActiveRecord::TestCase
   fixtures :topics, :posts
 
+  setup do
+    ActiveRecord::Base.use_yaml_unsafe_load = true
+  end
+
   MyObject = Struct.new :attribute1, :attribute2
 
   class Topic < ActiveRecord::Base
@@ -399,3 +403,76 @@ def test_serialized_attribute_works_under_concurrent_initial_access
     assert_equal [1] * threads.size, threads.map(&:value)
   end
 end
+
+class SerializedAttributeTestWithYAMLSafeLoad < SerializedAttributeTest
+  setup do
+    ActiveRecord::Base.use_yaml_unsafe_load = false
+  end
+
+  def test_serialized_attributes_from_database_on_subclass
+    Topic.serialize :content, Hash
+
+    t = ImportantTopic.new(content: { "foo" => "bar" })
+    assert_equal({ "foo" => "bar" }, t.content)
+    t.save!
+    t = ImportantTopic.last
+    assert_equal({ "foo" => "bar" }, t.content)
+  end
+
+  def test_should_raise_exception_on_serialized_attribute_with_type_mismatch
+    myobj = String.new("value1")
+    topic = Topic.new(content: myobj)
+    assert topic.save
+    Topic.serialize(:content, Hash)
+    assert_raise(ActiveRecord::SerializationTypeMismatch) { Topic.find(topic.id).content }
+  end
+
+  def test_serialized_attribute
+    Topic.serialize("content", String)
+
+    myobj = String.new("value1")
+    topic = Topic.create("content" => myobj)
+    assert_equal(myobj, topic.content)
+
+    topic.reload
+    assert_equal(myobj, topic.content)
+  end
+
+  def test_unexpected_serialized_type
+    Topic.serialize :content, Hash
+    topic = Topic.create!(content: { "zomg" => true })
+
+    Topic.serialize :content, Array
+
+    topic.reload
+    error = assert_raise(ActiveRecord::SerializationTypeMismatch) do
+      topic.content
+    end
+    expected = "can't load `content`: was supposed to be a Array, but was a Hash. -- {\"zomg\"=>true}"
+    assert_equal expected, error.to_s
+  end
+
+  def test_serialize_attribute_via_select_method_when_time_zone_available
+    with_timezone_config aware_attributes: true do
+      Topic.serialize(:content, String)
+
+      myobj = String.new("value1")
+      topic = Topic.create(content: myobj)
+
+      assert_equal(myobj, Topic.select(:content).find(topic.id).content)
+      assert_raise(ActiveModel::MissingAttributeError) { Topic.select(:id).find(topic.id).content }
+    end
+  end
+
+  def test_nil_is_always_persisted_as_null
+    Topic.serialize(:content, Hash)
+
+    topic = Topic.create!(content: { "foo" => "bar" })
+    topic.update_attribute :content, nil
+    assert_equal [topic], Topic.where(content: nil)
+  end
+
+  def test_serialized_time_attribute
+    skip "Time is not a supported class in Psych::safe_load."
+  end
+end
\ No newline at end of file
diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb
index 91c0e959f4..5aef4378e6 100644
--- a/activerecord/test/cases/store_test.rb
+++ b/activerecord/test/cases/store_test.rb
@@ -3,6 +3,7 @@
 require "cases/helper"
 require "models/admin"
 require "models/admin/user"
+require "models/admin/user_json"
 
 class StoreTest < ActiveRecord::TestCase
   fixtures :'admin/users'
@@ -14,6 +15,7 @@ class StoreTest < ActiveRecord::TestCase
       parent_name: "Quinn", partner_name: "Dallas",
       partner_birthday: "1997-11-1"
     )
+    ActiveRecord::Base.use_yaml_unsafe_load = true
   end
 
   test "reading store attributes through accessors" do
@@ -178,7 +180,7 @@ class StoreTest < ActiveRecord::TestCase
     assert_equal "heavy", @john.json_data["weight"]
   end
 
-  test "convert store attributes from Hash to HashWithIndifferentAccess saving the data and access attributes indifferently" do
+ def test_convert_store_attributes_from_Hash_to_HashWithIndifferentAccess_saving_the_data_and_access_attributes_indifferently
     user = Admin::User.find_by_name("Jamis")
     assert_equal "symbol",  user.settings[:symbol]
     assert_equal "symbol",  user.settings["symbol"]
@@ -317,3 +319,21 @@ class StoreTest < ActiveRecord::TestCase
     assert_equal [:secret_question, :two_factor_auth, :login_retry], Admin::User.stored_attributes[:configs]
   end
 end
+
+class StoreTestWithYAMLSafeLoad < StoreTest
+  fixtures :'admin/users'
+
+  setup do
+    @john = Admin::UserJSON.create!(
+      name: "Jim Doe", color: "black", remember_login: true,
+      height: "tall", is_a_good_guy: true,
+      parent_name: "Quinn", partner_name: "Dallas",
+      partner_birthday: "1997-11-1"
+    )
+    ActiveRecord::Base.use_yaml_unsafe_load = false
+  end
+
+  def test_convert_store_attributes_from_Hash_to_HashWithIndifferentAccess_saving_the_data_and_access_attributes_indifferently
+    skip "Symbol is not a supported class in Psych::safe_load"
+  end
+end
\ No newline at end of file
diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb
index 7003afa33a..32b6c154c9 100644
--- a/activerecord/test/cases/yaml_serialization_test.rb
+++ b/activerecord/test/cases/yaml_serialization_test.rb
@@ -24,8 +24,8 @@ def test_roundtrip
   end
 
   def test_roundtrip_serialized_column
-    topic = Topic.new(content: { omg: :lol })
-    assert_equal({ omg: :lol }, YAML.load(YAML.dump(topic)).content)
+    topic = Topic.new(content: { "omg" => "lol" })
+    assert_equal({ "omg" => "lol" }, YAML.load(YAML.dump(topic)).content)
   end
 
   def test_psych_roundtrip
@@ -94,12 +94,12 @@ def test_a_yaml_version_is_provided_for_future_backwards_compat
   end
 
   def test_deserializing_rails_41_yaml
-    topic = YAML.load(yaml_fixture("rails_4_1"))
+    topic = YAML.load(yaml_fixture("rails_4_1_no_symbol"))
 
     assert_predicate topic, :new_record?
     assert_nil topic.id
     assert_equal "The First Topic", topic.title
-    assert_equal({ omg: :lol }, topic.content)
+    assert_equal({ "omg" => "lol" }, topic.content)
   end
 
   def test_deserializing_rails_4_2_0_yaml
diff --git a/activerecord/test/models/admin/user_json.rb b/activerecord/test/models/admin/user_json.rb
new file mode 100644
index 0000000000..02469a43f7
--- /dev/null
+++ b/activerecord/test/models/admin/user_json.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+class Admin::UserJSON < ActiveRecord::Base
+  class Coder
+    def initialize(default = {})
+      @default = default
+    end
+
+    def dump(o)
+      ActiveSupport::JSON.encode(o || @default)
+    end
+
+    def load(s)
+      s.present? ? ActiveSupport::JSON.decode(s) : @default.clone
+    end
+  end
+
+  belongs_to :account
+  store_accessor :settings, :favorite_food
+  store_accessor :spouse, :name, prefix: :partner
+  store_accessor :configs, :login_retry, suffix: :config
+  store :params, accessors: [ :token ], coder: JSON
+  store :settings, accessors: [ :color, :homepage ], coder: Coder.new
+  store :parent, accessors: [:birthday, :name], prefix: true, coder: Coder.new
+  store :spouse, accessors: [:birthday], prefix: :partner, coder: Coder.new
+  store :configs, accessors: [ :secret_question ], coder: Coder.new
+  store :configs, accessors: [ :two_factor_auth ], suffix: true, coder: Coder.new
+  store :preferences, accessors: [ :remember_login ], coder: Coder.new
+  store :json_data, accessors: [ :height, :weight ], coder: Coder.new
+  store :json_data_empty, accessors: [ :is_a_good_guy ], coder: Coder.new
+
+  def phone_number
+    read_store_attribute(:settings, :phone_number).gsub(/(\d{3})(\d{3})(\d{4})/, '(\1) \2-\3')
+  end
+
+  def phone_number=(value)
+    write_store_attribute(:settings, :phone_number, value && value.gsub(/[^\d]/, ""))
+  end
+
+  def color
+    super || "red"
+  end
+
+  def color=(value)
+    value = "blue" unless %w(black red green blue).include?(value)
+    super
+  end
+end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index be2277dbe8..3641c7b931 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -41,6 +41,21 @@
     t.references :account
   end
 
+  create_table :admin_user_jsons, force: true do |t|
+    t.string :name
+    t.string :settings, null: true, limit: 1024
+    t.string :parent, null: true, limit: 1024
+    t.string :spouse, null: true, limit: 1024
+    t.string :configs, null: true, limit: 1024
+    # MySQL does not allow default values for blobs. Fake it out with a
+    # big varchar below.
+    t.string :preferences, null: true, default: "", limit: 1024
+    t.string :json_data, null: true, limit: 1024
+    t.string :json_data_empty, null: true, default: "", limit: 1024
+    t.text :params
+    t.references :account
+  end
+
   create_table :aircraft, force: true do |t|
     t.string :name
     t.integer :wheels_count, default: 0, null: false
diff --git a/activerecord/test/support/yaml_compatibility_fixtures/rails_4_1_no_symbol.yml b/activerecord/test/support/yaml_compatibility_fixtures/rails_4_1_no_symbol.yml
new file mode 100644
index 0000000000..14940b59ce
--- /dev/null
+++ b/activerecord/test/support/yaml_compatibility_fixtures/rails_4_1_no_symbol.yml
@@ -0,0 +1,22 @@
+--- !ruby/object:Topic
+  attributes:
+    id:
+    title: The First Topic
+    author_name: David
+    author_email_address: david@loudthinking.com
+    written_on: 2003-07-16 14:28:11.223300000 Z
+    bonus_time: 2000-01-01 14:28:00.000000000 Z
+    last_read: 2004-04-15
+    content: |
+      ---
+      omg: lol
+    important:
+    approved: false
+    replies_count: 1
+    unique_replies_count: 0
+    parent_id:
+    parent_title:
+    type:
+    group:
+    created_at: 2015-03-10 17:05:42.000000000 Z
+    updated_at: 2015-03-10 17:05:42.000000000 Z
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index 24836737a3..b022d9717a 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -1597,6 +1597,39 @@ def index
       assert_not ActiveRecord::Base.verbose_query_logs
     end
 
+    test "config.active_record.use_yaml_unsafe_load defaults to false" do
+      app "production"
+
+      assert_not ActiveRecord::Base.use_yaml_unsafe_load
+    end
+
+    test "config.active_record.use_yaml_unsafe_load can be configured" do
+      remove_from_config '.*config\.load_defaults.*\n'
+
+      app_file "config/initializers/use_yaml_unsafe_load.rb", <<-RUBY
+        Rails.application.config.active_record.use_yaml_unsafe_load = true
+      RUBY
+
+      app "production"
+      assert ActiveRecord::Base.use_yaml_unsafe_load
+    end
+
+    test "config.active_record.yaml_column_permitted_classes is [] by default" do
+      app "production"
+      assert_equal([], ActiveRecord::Base.yaml_column_permitted_classes)
+    end
+
+    test "config.active_record.yaml_column_permitted_classes can be configured" do
+      remove_from_config '.*config\.load_defaults.*\n'
+
+      app_file "config/initializers/yaml_permitted_classes.rb", <<-RUBY
+        Rails.application.config.active_record.yaml_column_permitted_classes = [Symbol]
+      RUBY
+
+      app "production"
+      assert_equal([Symbol], ActiveRecord::Base.yaml_column_permitted_classes)
+    end
+
     test "config.annotations wrapping SourceAnnotationExtractor::Annotation class" do
       make_basic_app do |application|
         application.config.annotations.register_extensions("coffee") do |tag|
-- 
2.36.0

