Skip to content

Commit b08a710

Browse files
hsbtclaude
andcommitted
Reject non-Hash classes for hash-with-ivars tags
!ruby/hash-with-ivars, !ruby/hash and !map are only emitted for Hash subclasses, but the loader allocated whatever class the tag named and populated its ivars directly. That let a permitted non-Hash class be instantiated with attacker-chosen ivars, bypassing its init_with validation. Verify the resolved class is a Hash subclass before use. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent f2e4b9d commit b08a710

2 files changed

Lines changed: 40 additions & 2 deletions

File tree

lib/psych/visitors/to_ruby.rb

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -302,7 +302,7 @@ def visit_Psych_Nodes_Mapping o
302302
set
303303

304304
when /^!ruby\/hash-with-ivars(?::(.*))?$/
305-
hash = $1 ? resolve_class($1).allocate : {}
305+
hash = $1 ? resolve_subclass($1, Hash).allocate : {}
306306
register o, hash
307307
o.children.each_slice(2) do |key, value|
308308
case key.value
@@ -317,7 +317,7 @@ def visit_Psych_Nodes_Mapping o
317317
hash
318318

319319
when /^!map:(.*)$/, /^!ruby\/hash:(.*)$/
320-
revive_hash register(o, resolve_class($1).allocate), o
320+
revive_hash register(o, resolve_subclass($1, Hash).allocate), o
321321

322322
when '!omap', 'tag:yaml.org,2002:omap'
323323
map = register(o, class_loader.psych_omap.new)
@@ -469,6 +469,19 @@ def init_with o, h, node
469469
def resolve_class klassname
470470
class_loader.load klassname
471471
end
472+
473+
# Resolve +klassname+ and ensure it is +parent+ or one of its
474+
# subclasses. Tags such as !ruby/hash-with-ivars are only ever emitted
475+
# for subclasses of a specific core class; without this check a crafted
476+
# document could name an unrelated (but permitted) class and have its
477+
# state populated directly, bypassing the class's own init_with.
478+
def resolve_subclass klassname, parent
479+
klass = resolve_class(klassname)
480+
if klass && !(klass <= parent)
481+
raise ArgumentError, "Invalid tag: expected a subclass of #{parent}, got #{klass}"
482+
end
483+
klass
484+
end
472485
end
473486

474487
class NoAliasRuby < ToRuby

test/psych/test_hash.rb

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,31 @@ def test_map
9292
assert_equal X, x.class
9393
end
9494

95+
class NotAHash
96+
def init_with(coder)
97+
@string = coder.map["string"].to_s
98+
end
99+
end
100+
101+
def test_hash_with_ivars_rejects_non_hash_class
102+
assert_raise(ArgumentError) do
103+
Psych.unsafe_load <<~eoyml
104+
--- !ruby/hash-with-ivars:#{NotAHash}
105+
ivars:
106+
'@string': ["a surprise array!"]
107+
eoyml
108+
end
109+
end
110+
111+
def test_hash_tag_rejects_non_hash_class
112+
assert_raise(ArgumentError) do
113+
Psych.unsafe_load "--- !ruby/hash:#{NotAHash} {}\n"
114+
end
115+
assert_raise(ArgumentError) do
116+
Psych.unsafe_load "--- !map:#{NotAHash} {}\n"
117+
end
118+
end
119+
95120
def test_self_referential
96121
@hash['self'] = @hash
97122
assert_cycle(@hash)

0 commit comments

Comments
 (0)