YAMLColumn: use `YAML.safe_dump` if available

One particularly annoying thing with YAMLColumn type restriction
is that it is only checked on load.

Which means if your code insert data with unsupported types, the
insert will work, but now you'll be unable to read the record, which
makes it hard to fix etc.

That's the reason why I implemented `YAML.safe_dump` (https://github.com/ruby/psych/pull/495).

It applies exactly the same restrictions than `safe_load`, which means
if you attempt to store non-permitted fields, it will fail on insertion
and not on further reads, so you won't create an invalid record in your
database.
This commit is contained in:
Jean Boussier 2023-01-22 08:32:03 -05:00
parent d6eec533c1
commit 3635d2d9cf
4 changed files with 40 additions and 5 deletions

View File

@ -333,7 +333,7 @@ GEM
activesupport (>= 7.0.0)
rack
railties (>= 7.0.0)
psych (5.0.1)
psych (5.1.0)
stringio
public_suffix (5.0.1)
puma (6.0.2)

View File

@ -1,3 +1,14 @@
* YAML columns use `YAML.safe_dump` is available.
As of `psych 5.1.0`, `YAML.safe_dump` can now apply the same permitted
types restrictions than `YAML.safe_load`.
It's preferable to ensure the payload only use allowed types when we first
try to serialize it, otherwise you may end up with invalid records in the
database.
*Jean Boussier*
* `ActiveRecord::QueryLogs` better handle broken encoding.
It's not uncommon when building queries with BLOB fields to contain

View File

@ -26,12 +26,25 @@ module ActiveRecord
@unsafe_load = coder["unsafe_load"]
end
if Gem::Version.new(Psych::VERSION) >= "5.1"
def dump(obj)
return if obj.nil?
assert_valid_value(obj, action: "dump")
if unsafe_load?
YAML.dump(obj)
else
YAML.safe_dump(obj, permitted_classes: permitted_classes, aliases: true)
end
end
else
def dump(obj)
return if obj.nil?
assert_valid_value(obj, action: "dump")
YAML.dump obj
end
end
def load(yaml)
return object_class.new if object_class != Object && yaml.nil?

View File

@ -97,6 +97,17 @@ module ActiveRecord
end
end
def test_yaml_column_permitted_classes_are_consumed_by_safe_dump
if Gem::Version.new(Psych::VERSION) < "5.1"
skip "YAML.safe_dump is either missing on unavailable on #{Psych::VERSION}"
end
coder = YAMLColumn.new("attr_name")
assert_raises(Psych::DisallowedClass) do
coder.dump([Time.new])
end
end
def test_yaml_column_permitted_classes_option
ActiveRecord.yaml_column_permitted_classes = [Symbol]