Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/psych/class_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
26 changes: 20 additions & 6 deletions lib/psych/visitors/to_ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand All @@ -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]
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Comment on lines +478 to +484
end

class NoAliasRuby < ToRuby
Expand Down
12 changes: 12 additions & 0 deletions test/psych/test_array.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
25 changes: 25 additions & 0 deletions test/psych/test_hash.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
13 changes: 13 additions & 0 deletions test/psych/test_safe_load.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
12 changes: 12 additions & 0 deletions test/psych/test_string.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading