Skip to content

Commit cb50aa8

Browse files
committed
Introduce Psych.unsafe_load
In future versions of Psych, the `load` method will be mostly the same as the `safe_load` method. In other words, the `load` method won't allow arbitrary object deserialization (which can be used to escalate to an RCE). People that need to load *trusted* documents can use the `unsafe_load` method. This commit introduces the `unsafe_load` method so that people can incrementally upgrade. For example, if they try to upgrade to 4.0.0 and something breaks, they can downgrade, audit callsites, change to `safe_load` or `unsafe_load` as required, and then upgrade to 4.0.0 smoothly.
1 parent 64bee7e commit cb50aa8

26 files changed

+156
-123
lines changed

lib/psych.rb

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,7 +271,7 @@ module Psych
271271
# YAML documents that are supplied via user input. Instead, please use the
272272
# safe_load method.
273273
#
274-
def self.load yaml, legacy_filename = NOT_GIVEN, filename: nil, fallback: false, symbolize_names: false, freeze: false
274+
def self.unsafe_load yaml, legacy_filename = NOT_GIVEN, filename: nil, fallback: false, symbolize_names: false, freeze: false
275275
if legacy_filename != NOT_GIVEN
276276
warn_with_uplevel 'Passing filename with the 2nd argument of Psych.load is deprecated. Use keyword argument like Psych.load(yaml, filename: ...) instead.', uplevel: 1 if $VERBOSE
277277
filename = legacy_filename
@@ -281,6 +281,7 @@ def self.load yaml, legacy_filename = NOT_GIVEN, filename: nil, fallback: false,
281281
return fallback unless result
282282
result.to_ruby(symbolize_names: symbolize_names, freeze: freeze)
283283
end
284+
class << self; alias :load :unsafe_load; end
284285

285286
###
286287
# Safely load the yaml string in +yaml+. By default, only the following
@@ -577,11 +578,12 @@ def self.load_stream yaml, legacy_filename = NOT_GIVEN, filename: nil, fallback:
577578
# NOTE: This method *should not* be used to parse untrusted documents, such as
578579
# YAML documents that are supplied via user input. Instead, please use the
579580
# safe_load_file method.
580-
def self.load_file filename, **kwargs
581+
def self.unsafe_load_file filename, **kwargs
581582
File.open(filename, 'r:bom|utf-8') { |f|
582-
self.load f, filename: filename, **kwargs
583+
self.unsafe_load f, filename: filename, **kwargs
583584
}
584585
end
586+
class << self; alias :load_file :unsafe_load_file; end
585587

586588
###
587589
# Safely loads the document contained in +filename+. Returns the yaml contained in

lib/psych/versions.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
21
# frozen_string_literal: true
2+
33
module Psych
44
# The version of Psych you are using
5-
VERSION = '3.3.1'
5+
VERSION = '3.3.2'
66

77
if RUBY_ENGINE == 'jruby'
88
DEFAULT_SNAKEYAML_VERSION = '1.28'.freeze

test/psych/helper.rb

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -41,24 +41,30 @@ def with_default_internal(enc)
4141
# Convert between Psych and the object to verify correct parsing and
4242
# emitting
4343
#
44-
def assert_to_yaml( obj, yaml )
45-
assert_equal( obj, Psych::load( yaml ) )
44+
def assert_to_yaml( obj, yaml, loader = :load )
45+
assert_equal( obj, Psych.send(loader, yaml) )
4646
assert_equal( obj, Psych::parse( yaml ).transform )
47-
assert_equal( obj, Psych::load( obj.to_yaml ) )
47+
assert_equal( obj, Psych.send(loader, obj.to_yaml) )
4848
assert_equal( obj, Psych::parse( obj.to_yaml ).transform )
49-
assert_equal( obj, Psych::load(
49+
assert_equal( obj, Psych.send(loader,
5050
obj.to_yaml(
5151
:UseVersion => true, :UseHeader => true, :SortKeys => true
5252
)
5353
))
54+
rescue Psych::DisallowedClass, Psych::BadAlias
55+
assert_to_yaml obj, yaml, :unsafe_load
5456
end
5557

5658
#
5759
# Test parser only
5860
#
5961
def assert_parse_only( obj, yaml )
60-
assert_equal( obj, Psych::load( yaml ) )
61-
assert_equal( obj, Psych::parse( yaml ).transform )
62+
begin
63+
assert_equal obj, Psych::load( yaml )
64+
rescue Psych::DisallowedClass, Psych::BadAlias
65+
assert_equal obj, Psych::unsafe_load( yaml )
66+
end
67+
assert_equal obj, Psych::parse( yaml ).transform
6268
end
6369

6470
def assert_cycle( obj )
@@ -69,9 +75,15 @@ def assert_cycle( obj )
6975
assert_nil Psych::load(Psych.dump(obj))
7076
assert_nil Psych::load(obj.to_yaml)
7177
else
72-
assert_equal(obj, Psych.load(v.tree.yaml))
73-
assert_equal(obj, Psych::load(Psych.dump(obj)))
74-
assert_equal(obj, Psych::load(obj.to_yaml))
78+
begin
79+
assert_equal(obj, Psych.load(v.tree.yaml))
80+
assert_equal(obj, Psych::load(Psych.dump(obj)))
81+
assert_equal(obj, Psych::load(obj.to_yaml))
82+
rescue Psych::DisallowedClass, Psych::BadAlias
83+
assert_equal(obj, Psych.unsafe_load(v.tree.yaml))
84+
assert_equal(obj, Psych::unsafe_load(Psych.dump(obj)))
85+
assert_equal(obj, Psych::unsafe_load(obj.to_yaml))
86+
end
7587
end
7688
end
7789

test/psych/test_alias_and_anchor.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def test_mri_compatibility
1919
- *id001
2020
- *id001
2121
EOYAML
22-
result = Psych.load yaml
22+
result = Psych.unsafe_load yaml
2323
result.each {|el| assert_same(result[0], el) }
2424
end
2525

@@ -33,7 +33,7 @@ def test_mri_compatibility_object_with_ivars
3333
- *id001
3434
EOYAML
3535

36-
result = Psych.load yaml
36+
result = Psych.unsafe_load yaml
3737
result.each do |el|
3838
assert_same(result[0], el)
3939
assert_equal('test1', el.var1)
@@ -50,7 +50,7 @@ def test_mri_compatibility_substring_with_ivars
5050
- *id001
5151
- *id001
5252
EOYAML
53-
result = Psych.load yaml
53+
result = Psych.unsafe_load yaml
5454
result.each do |el|
5555
assert_same(result[0], el)
5656
assert_equal('test', el.var1)
@@ -62,7 +62,7 @@ def test_anchor_alias_round_trip
6262
original = [o,o,o]
6363

6464
yaml = Psych.dump original
65-
result = Psych.load yaml
65+
result = Psych.unsafe_load yaml
6666
result.each {|el| assert_same(result[0], el) }
6767
end
6868

@@ -73,7 +73,7 @@ def test_anchor_alias_round_trip_object_with_ivars
7373
original = [o,o,o]
7474

7575
yaml = Psych.dump original
76-
result = Psych.load yaml
76+
result = Psych.unsafe_load yaml
7777
result.each do |el|
7878
assert_same(result[0], el)
7979
assert_equal('test1', el.var1)
@@ -87,7 +87,7 @@ def test_anchor_alias_round_trip_substring_with_ivars
8787
original = [o,o,o]
8888

8989
yaml = Psych.dump original
90-
result = Psych.load yaml
90+
result = Psych.unsafe_load yaml
9191
result.each do |el|
9292
assert_same(result[0], el)
9393
assert_equal('test', el.var1)

test/psych/test_array.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ def test_enumerator
2424
def test_another_subclass_with_attributes
2525
y = Y.new.tap {|o| o.val = 1}
2626
y << "foo" << "bar"
27-
y = Psych.load Psych.dump y
27+
y = Psych.unsafe_load Psych.dump y
2828

2929
assert_equal %w{foo bar}, y
3030
assert_equal Y, y.class
@@ -42,13 +42,13 @@ def test_subclass
4242
end
4343

4444
def test_subclass_with_attributes
45-
y = Psych.load Psych.dump Y.new.tap {|o| o.val = 1}
45+
y = Psych.unsafe_load Psych.dump Y.new.tap {|o| o.val = 1}
4646
assert_equal Y, y.class
4747
assert_equal 1, y.val
4848
end
4949

5050
def test_backwards_with_syck
51-
x = Psych.load "--- !seq:#{X.name} []\n\n"
51+
x = Psych.unsafe_load "--- !seq:#{X.name} []\n\n"
5252
assert_equal X, x.class
5353
end
5454

test/psych/test_coder.rb

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ def encode_with(coder)
124124

125125
def test_self_referential
126126
x = Referential.new
127-
copy = Psych.load Psych.dump x
127+
copy = Psych.unsafe_load Psych.dump x
128128
assert_equal copy, copy.a
129129
end
130130

@@ -163,23 +163,23 @@ def test_map_with_tag_and_style
163163
end
164164

165165
def test_represent_map
166-
thing = Psych.load(Psych.dump(RepresentWithMap.new))
166+
thing = Psych.unsafe_load(Psych.dump(RepresentWithMap.new))
167167
assert_equal({ "string" => 'a', :symbol => 'b' }, thing.map)
168168
end
169169

170170
def test_represent_sequence
171-
thing = Psych.load(Psych.dump(RepresentWithSeq.new))
171+
thing = Psych.unsafe_load(Psych.dump(RepresentWithSeq.new))
172172
assert_equal %w{ foo bar }, thing.seq
173173
end
174174

175175
def test_represent_with_init
176-
thing = Psych.load(Psych.dump(RepresentWithInit.new))
176+
thing = Psych.unsafe_load(Psych.dump(RepresentWithInit.new))
177177
assert_equal 'bar', thing.str
178178
end
179179

180180
def test_represent!
181181
assert_match(/foo/, Psych.dump(Represent.new))
182-
assert_instance_of(Represent, Psych.load(Psych.dump(Represent.new)))
182+
assert_instance_of(Represent, Psych.unsafe_load(Psych.dump(Represent.new)))
183183
end
184184

185185
def test_scalar_coder
@@ -189,7 +189,7 @@ def test_scalar_coder
189189

190190
def test_load_dumped_tagging
191191
foo = InitApi.new
192-
bar = Psych.load(Psych.dump(foo))
192+
bar = Psych.unsafe_load(Psych.dump(foo))
193193
assert_equal false, bar.implicit
194194
assert_equal "!ruby/object:Psych::TestCoder::InitApi", bar.tag
195195
assert_equal Psych::Nodes::Mapping::BLOCK, bar.style
@@ -208,7 +208,7 @@ def test_dump_encode_with
208208

209209
def test_dump_init_with
210210
foo = InitApi.new
211-
bar = Psych.load(Psych.dump(foo))
211+
bar = Psych.unsafe_load(Psych.dump(foo))
212212
assert_equal foo.a, bar.a
213213
assert_equal foo.b, bar.b
214214
assert_nil bar.c

test/psych/test_date_time.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def test_non_utc
2222
def test_timezone_offset
2323
times = [Time.new(2017, 4, 13, 12, 0, 0, "+09:00"),
2424
Time.new(2017, 4, 13, 12, 0, 0, "-05:00")]
25-
cycled = Psych::load(Psych.dump times)
25+
cycled = Psych::unsafe_load(Psych.dump times)
2626
assert_match(/12:00:00 \+0900/, cycled.first.to_s)
2727
assert_match(/12:00:00 -0500/, cycled.last.to_s)
2828
end
@@ -39,7 +39,7 @@ def test_datetime_non_utc
3939
def test_datetime_timezone_offset
4040
times = [DateTime.new(2017, 4, 13, 12, 0, 0, "+09:00"),
4141
DateTime.new(2017, 4, 13, 12, 0, 0, "-05:00")]
42-
cycled = Psych::load(Psych.dump times)
42+
cycled = Psych::unsafe_load(Psych.dump times)
4343
assert_match(/12:00:00\+09:00/, cycled.first.to_s)
4444
assert_match(/12:00:00-05:00/, cycled.last.to_s)
4545
end

test/psych/test_deprecated.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ def to_yaml opts = {}
4141
def test_recursive_quick_emit_encode_with
4242
qeew = QuickEmitterEncodeWith.new
4343
hash = { :qe => qeew }
44-
hash2 = Psych.load Psych.dump hash
44+
hash2 = Psych.unsafe_load Psych.dump hash
4545
qe = hash2[:qe]
4646

4747
assert_equal qeew.name, qe.name
@@ -72,7 +72,7 @@ def yaml_initialize tag, vals
7272
# receive the yaml_initialize call.
7373
def test_yaml_initialize_and_init_with
7474
hash = { :yi => YamlInitAndInitWith.new }
75-
hash2 = Psych.load Psych.dump hash
75+
hash2 = Psych.unsafe_load Psych.dump hash
7676
yi = hash2[:yi]
7777

7878
assert_equal 'TGIF!', yi.name

test/psych/test_exception.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,13 @@ def make_ex msg = 'oh no!'
3333

3434
def test_backtrace
3535
err = make_ex
36-
new_err = Psych.load(Psych.dump(err))
36+
new_err = Psych.unsafe_load(Psych.dump(err))
3737
assert_equal err.backtrace, new_err.backtrace
3838
end
3939

4040
def test_naming_exception
4141
err = String.xxx rescue $!
42-
new_err = Psych.load(Psych.dump(err))
42+
new_err = Psych.unsafe_load(Psych.dump(err))
4343
assert_equal err.message, new_err.message
4444
end
4545

@@ -56,7 +56,7 @@ def test_load_takes_file
5656

5757
# deprecated interface
5858
ex = assert_raise(Psych::SyntaxError) do
59-
Psych.load '--- `', 'deprecated'
59+
Psych.unsafe_load '--- `', 'deprecated'
6060
end
6161
assert_equal 'deprecated', ex.file
6262
end
@@ -165,7 +165,7 @@ def test_attributes
165165
end
166166

167167
def test_convert
168-
w = Psych.load(Psych.dump(@wups))
168+
w = Psych.unsafe_load(Psych.dump(@wups))
169169
assert_equal @wups.message, w.message
170170
assert_equal @wups.backtrace, w.backtrace
171171
assert_equal 1, w.foo

test/psych/test_hash.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def setup
3939
def test_hash_with_ivar
4040
t1 = HashWithIvar.new
4141
t1[:foo] = :bar
42-
t2 = Psych.load(Psych.dump(t1))
42+
t2 = Psych.unsafe_load(Psych.dump(t1))
4343
assert_equal t1, t2
4444
assert_cycle t1
4545
end
@@ -54,14 +54,14 @@ def test_referenced_hash_with_ivar
5454
def test_custom_initialized
5555
a = [1,2,3,4,5]
5656
t1 = HashWithCustomInit.new(a)
57-
t2 = Psych.load(Psych.dump(t1))
57+
t2 = Psych.unsafe_load(Psych.dump(t1))
5858
assert_equal t1, t2
5959
assert_cycle t1
6060
end
6161

6262
def test_custom_initialize_no_ivar
6363
t1 = HashWithCustomInitNoIvar.new(nil)
64-
t2 = Psych.load(Psych.dump(t1))
64+
t2 = Psych.unsafe_load(Psych.dump(t1))
6565
assert_equal t1, t2
6666
assert_cycle t1
6767
end
@@ -70,25 +70,25 @@ def test_hash_subclass_with_ivars
7070
x = X.new
7171
x[:a] = 'b'
7272
x.instance_variable_set :@foo, 'bar'
73-
dup = Psych.load Psych.dump x
73+
dup = Psych.unsafe_load Psych.dump x
7474
assert_cycle x
7575
assert_equal 'bar', dup.instance_variable_get(:@foo)
7676
assert_equal X, dup.class
7777
end
7878

7979
def test_load_with_class_syck_compatibility
80-
hash = Psych.load "--- !ruby/object:Hash\n:user_id: 7\n:username: Lucas\n"
80+
hash = Psych.unsafe_load "--- !ruby/object:Hash\n:user_id: 7\n:username: Lucas\n"
8181
assert_equal({ user_id: 7, username: 'Lucas'}, hash)
8282
end
8383

8484
def test_empty_subclass
8585
assert_match "!ruby/hash:#{X}", Psych.dump(X.new)
86-
x = Psych.load Psych.dump X.new
86+
x = Psych.unsafe_load Psych.dump X.new
8787
assert_equal X, x.class
8888
end
8989

9090
def test_map
91-
x = Psych.load "--- !map:#{X} { }\n"
91+
x = Psych.unsafe_load "--- !map:#{X} { }\n"
9292
assert_equal X, x.class
9393
end
9494

@@ -102,7 +102,7 @@ def test_cycles
102102
end
103103

104104
def test_ref_append
105-
hash = Psych.load(<<-eoyml)
105+
hash = Psych.unsafe_load(<<-eoyml)
106106
---
107107
foo: &foo
108108
hello: world

0 commit comments

Comments
 (0)