diff --git a/lib/psych/class_loader.rb b/lib/psych/class_loader.rb index c8f50972..3a866ae4 100644 --- a/lib/psych/class_loader.rb +++ b/lib/psych/class_loader.rb @@ -9,6 +9,7 @@ class ClassLoader # :nodoc: DATA = 'Data' unless RUBY_VERSION < "3.2" DATE = 'Date' DATE_TIME = 'DateTime' + ENCODING = 'Encoding' EXCEPTION = 'Exception' OBJECT = 'Object' PSYCH_OMAP = 'Psych::Omap' diff --git a/lib/psych/visitors/to_ruby.rb b/lib/psych/visitors/to_ruby.rb index 475444e5..79565dfc 100644 --- a/lib/psych/visitors/to_ruby.rb +++ b/lib/psych/visitors/to_ruby.rb @@ -72,7 +72,7 @@ def deserialize o when '!binary', 'tag:yaml.org,2002:binary' o.value.unpack('m').first when /^!(?:str|ruby\/string)(?::(.*))?$/, 'tag:yaml.org,2002:str' - klass = resolve_class($1) + klass = resolve_subclass($1, String) if klass klass.allocate.replace o.value else @@ -87,6 +87,7 @@ def deserialize o DateTime.civil(*t.to_a[0, 6].reverse, Rational(t.utc_offset, 86400)) + (t.subsec/86400) when '!ruby/encoding' + class_loader.encoding ::Encoding.find o.value when "!ruby/object:Complex" class_loader.complex @@ -156,7 +157,7 @@ def visit_Psych_Nodes_Sequence o } map when /^!(?:seq|ruby\/array):(.*)$/ - klass = resolve_class($1) + klass = resolve_subclass($1, Array) list = register(o, klass.allocate) o.children.each { |c| list.push accept c } list @@ -246,7 +247,7 @@ def visit_Psych_Nodes_Mapping o end when /^!(?:str|ruby\/string)(?::(.*))?$/, 'tag:yaml.org,2002:str' - klass = resolve_class($1) + klass = resolve_subclass($1, String) members = {} string = nil @@ -267,7 +268,7 @@ def visit_Psych_Nodes_Mapping o end init_with(string, members.map { |k,v| [k.to_s.sub(/^@/, ''),v] }, o) when /^!ruby\/array:(.*)$/ - klass = resolve_class($1) + klass = resolve_subclass($1, Array) list = register(o, klass.allocate) members = Hash[o.children.map { |c| accept c }.each_slice(2).to_a] @@ -301,7 +302,7 @@ def visit_Psych_Nodes_Mapping o set when /^!ruby\/hash-with-ivars(?::(.*))?$/ - hash = $1 ? resolve_class($1).allocate : {} + hash = $1 ? resolve_subclass($1, Hash).allocate : {} register o, hash o.children.each_slice(2) do |key, value| case key.value @@ -316,7 +317,7 @@ def visit_Psych_Nodes_Mapping o hash when /^!map:(.*)$/, /^!ruby\/hash:(.*)$/ - revive_hash register(o, resolve_class($1).allocate), o + revive_hash register(o, resolve_subclass($1, Hash).allocate), o when '!omap', 'tag:yaml.org,2002:omap' map = register(o, class_loader.psych_omap.new) @@ -468,6 +469,19 @@ def init_with o, h, node def resolve_class klassname class_loader.load klassname end + + # Resolve +klassname+ and ensure it is +parent+ or one of its + # subclasses. Tags such as !ruby/hash-with-ivars are only ever emitted + # for subclasses of a specific core class; without this check a crafted + # document could name an unrelated (but permitted) class and have its + # state populated directly, bypassing the class's own init_with. + def resolve_subclass klassname, parent + klass = resolve_class(klassname) + if klass && !(klass <= parent) + raise ArgumentError, "Invalid tag: expected a subclass of #{parent}, got #{klass}" + end + klass + end end class NoAliasRuby < ToRuby diff --git a/test/psych/test_array.rb b/test/psych/test_array.rb index 0dc82439..064ace46 100644 --- a/test/psych/test_array.rb +++ b/test/psych/test_array.rb @@ -52,6 +52,18 @@ def test_backwards_with_syck assert_equal X, x.class end + class NotAnArray + end + + def test_seq_tag_rejects_non_array_class + assert_raise(ArgumentError) do + Psych.unsafe_load "--- !seq:#{NotAnArray} []\n" + end + assert_raise(ArgumentError) do + Psych.unsafe_load "--- !ruby/array:#{NotAnArray} []\n" + end + end + def test_self_referential @list << @list assert_cycle(@list) diff --git a/test/psych/test_hash.rb b/test/psych/test_hash.rb index 31eba858..7f46c551 100644 --- a/test/psych/test_hash.rb +++ b/test/psych/test_hash.rb @@ -92,6 +92,31 @@ def test_map assert_equal X, x.class end + class NotAHash + def init_with(coder) + @string = coder.map["string"].to_s + end + end + + def test_hash_with_ivars_rejects_non_hash_class + assert_raise(ArgumentError) do + Psych.unsafe_load <<~eoyml + --- !ruby/hash-with-ivars:#{NotAHash} + ivars: + '@string': ["a surprise array!"] + eoyml + end + end + + def test_hash_tag_rejects_non_hash_class + assert_raise(ArgumentError) do + Psych.unsafe_load "--- !ruby/hash:#{NotAHash} {}\n" + end + assert_raise(ArgumentError) do + Psych.unsafe_load "--- !map:#{NotAHash} {}\n" + end + end + def test_self_referential @hash['self'] = @hash assert_cycle(@hash) diff --git a/test/psych/test_safe_load.rb b/test/psych/test_safe_load.rb index e6ca1e14..ac62df3b 100644 --- a/test/psych/test_safe_load.rb +++ b/test/psych/test_safe_load.rb @@ -74,6 +74,19 @@ def test_symbol assert_equal :foo, Psych.safe_load('--- !ruby/symbol foo', permitted_classes: [Symbol]) end + def test_encoding + yaml = "--- !ruby/encoding UTF-8\n" + assert_raise(Psych::DisallowedClass) do + Psych.safe_load yaml + end + assert_raise(Psych::DisallowedClass) do + Psych.safe_load yaml, permitted_classes: [] + end + + assert_equal Encoding::UTF_8, Psych.safe_load(yaml, permitted_classes: [Encoding]) + assert_equal Encoding::UTF_8, Psych.safe_load(yaml, permitted_classes: %w{ Encoding }) + end + def test_foo assert_raise(Psych::DisallowedClass) do Psych.safe_load '--- !ruby/object:Foo {}', permitted_classes: [Foo] diff --git a/test/psych/test_string.rb b/test/psych/test_string.rb index cfd235a5..73fb3933 100644 --- a/test/psych/test_string.rb +++ b/test/psych/test_string.rb @@ -182,6 +182,18 @@ def test_subclass_with_attributes assert_equal 1, y.val end + class NotAString + end + + def test_string_tag_rejects_non_string_class + assert_raise(ArgumentError) do + Psych.unsafe_load "--- !ruby/string:#{NotAString} foo\n" + end + assert_raise(ArgumentError) do + Psych.unsafe_load "--- !ruby/string:#{NotAString}\nstr: foo\n" + end + end + def test_string_with_base_60 yaml = Psych.dump '01:03:05' assert_match "'01:03:05'", yaml