safe_yaml 0.1 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.travis.yml +48 -0
  4. data/CHANGES.md +154 -0
  5. data/Gemfile +3 -1
  6. data/LICENSE.txt +22 -0
  7. data/README.md +191 -0
  8. data/Rakefile +22 -2
  9. data/bin/safe_yaml +75 -0
  10. data/bundle_install_all_ruby_versions.sh +11 -0
  11. data/lib/safe_yaml.rb +90 -6
  12. data/lib/safe_yaml/deep.rb +34 -0
  13. data/lib/safe_yaml/libyaml_checker.rb +36 -0
  14. data/lib/safe_yaml/load.rb +181 -0
  15. data/lib/safe_yaml/parse/date.rb +37 -0
  16. data/lib/safe_yaml/parse/hexadecimal.rb +12 -0
  17. data/lib/safe_yaml/parse/sexagesimal.rb +26 -0
  18. data/lib/safe_yaml/psych_handler.rb +99 -0
  19. data/lib/safe_yaml/psych_resolver.rb +52 -0
  20. data/lib/safe_yaml/resolver.rb +94 -0
  21. data/lib/safe_yaml/safe_to_ruby_visitor.rb +29 -0
  22. data/lib/safe_yaml/store.rb +39 -0
  23. data/lib/safe_yaml/syck_hack.rb +36 -0
  24. data/lib/safe_yaml/syck_node_monkeypatch.rb +43 -0
  25. data/lib/safe_yaml/syck_resolver.rb +38 -0
  26. data/lib/safe_yaml/transform.rb +41 -0
  27. data/lib/safe_yaml/transform/to_boolean.rb +21 -0
  28. data/lib/safe_yaml/transform/to_date.rb +13 -0
  29. data/lib/safe_yaml/transform/to_float.rb +33 -0
  30. data/lib/safe_yaml/transform/to_integer.rb +26 -0
  31. data/lib/safe_yaml/transform/to_nil.rb +18 -0
  32. data/lib/safe_yaml/transform/to_symbol.rb +17 -0
  33. data/lib/safe_yaml/transform/transformation_map.rb +47 -0
  34. data/lib/{version.rb → safe_yaml/version.rb} +1 -1
  35. data/run_specs_all_ruby_versions.sh +38 -0
  36. data/safe_yaml.gemspec +11 -8
  37. data/spec/exploit.1.9.2.yaml +2 -0
  38. data/spec/exploit.1.9.3.yaml +2 -0
  39. data/spec/issue48.txt +20 -0
  40. data/spec/issue49.yml +0 -0
  41. data/spec/libyaml_checker_spec.rb +69 -0
  42. data/spec/psych_resolver_spec.rb +10 -0
  43. data/spec/resolver_specs.rb +278 -0
  44. data/spec/safe_yaml_spec.rb +697 -23
  45. data/spec/spec_helper.rb +37 -2
  46. data/spec/store_spec.rb +57 -0
  47. data/spec/support/exploitable_back_door.rb +13 -7
  48. data/spec/syck_resolver_spec.rb +10 -0
  49. data/spec/transform/base64_spec.rb +11 -0
  50. data/spec/transform/to_date_spec.rb +60 -0
  51. data/spec/transform/to_float_spec.rb +42 -0
  52. data/spec/transform/to_integer_spec.rb +64 -0
  53. data/spec/transform/to_symbol_spec.rb +51 -0
  54. data/spec/yaml_spec.rb +15 -0
  55. metadata +78 -24
  56. data/Gemfile.lock +0 -28
  57. data/lib/handler.rb +0 -86
  58. data/spec/handler_spec.rb +0 -108
@@ -1,42 +1,118 @@
1
- require File.join(File.dirname(__FILE__), "spec_helper")
2
-
3
- require "safe_yaml"
4
- require "exploitable_back_door"
1
+ require "spec_helper"
5
2
 
6
3
  describe YAML do
4
+ def safe_load_round_trip(object, options={})
5
+ yaml = object.to_yaml
6
+ if SafeYAML::YAML_ENGINE == "psych"
7
+ YAML.safe_load(yaml, nil, options)
8
+ else
9
+ YAML.safe_load(yaml, options)
10
+ end
11
+ end
12
+
7
13
  before :each do
8
- ExploitableBackDoor.reset
14
+ # Need to require this here (as opposed to somewhere up higher in the file)
15
+ # to ensure that safe_yaml isn't loaded and therefore YAML isn't monkey-
16
+ # patched, for tests that require only safe_yaml/load.
17
+ require "safe_yaml"
18
+ require "exploitable_back_door"
19
+
20
+ SafeYAML.restore_defaults!
9
21
  end
10
22
 
11
- describe "load" do
12
- if RUBY_VERSION >= "1.9.3"
13
- it "allows exploits through objects defined in YAML w/ !ruby/hash" do
14
- YAML.load "--- !ruby/hash:ExploitableBackDoor\nfoo: bar\n"
15
- ExploitableBackDoor.should be_exploited
23
+ after :each do
24
+ SafeYAML.restore_defaults!
25
+ end
26
+
27
+ describe "unsafe_load" do
28
+ if SafeYAML::YAML_ENGINE == "psych" && RUBY_VERSION >= "1.9.3"
29
+ it "allows exploits through objects defined in YAML w/ !ruby/hash via custom :[]= methods" do
30
+ backdoor = YAML.unsafe_load("--- !ruby/hash:ExploitableBackDoor\nfoo: bar\n")
31
+ expect(backdoor).to be_exploited_through_setter
32
+ end
33
+
34
+ it "allows exploits through objects defined in YAML w/ !ruby/object via the :init_with method" do
35
+ backdoor = YAML.unsafe_load("--- !ruby/object:ExploitableBackDoor\nfoo: bar\n")
36
+ expect(backdoor).to be_exploited_through_init_with
16
37
  end
17
38
  end
18
39
 
19
- it "allows exploits through objects defined in YAML w/ !ruby/object" do
20
- YAML.load "--- !ruby/object:ExploitableBackDoor\nfoo: bar\n"
21
- ExploitableBackDoor.should be_exploited
40
+ it "allows exploits through objects w/ sensitive instance variables defined in YAML w/ !ruby/object" do
41
+ backdoor = YAML.unsafe_load("--- !ruby/object:ExploitableBackDoor\nfoo: bar\n")
42
+ expect(backdoor).to be_exploited_through_ivars
43
+ end
44
+
45
+ context "with special whitelisted tags defined" do
46
+ before :each do
47
+ SafeYAML::whitelist!(OpenStruct)
48
+ end
49
+
50
+ it "effectively ignores the whitelist (since everything is whitelisted)" do
51
+ result = YAML.unsafe_load <<-YAML.unindent
52
+ --- !ruby/object:OpenStruct
53
+ table:
54
+ :backdoor: !ruby/object:ExploitableBackDoor
55
+ foo: bar
56
+ YAML
57
+
58
+ expect(result).to be_a(OpenStruct)
59
+ expect(result.backdoor).to be_exploited_through_ivars
60
+ end
22
61
  end
23
62
  end
24
63
 
25
64
  describe "safe_load" do
65
+ it "does NOT allow exploits through objects defined in YAML w/ !ruby/hash" do
66
+ object = YAML.safe_load("--- !ruby/hash:ExploitableBackDoor\nfoo: bar\n")
67
+ expect(object).not_to be_a(ExploitableBackDoor)
68
+ end
69
+
26
70
  it "does NOT allow exploits through objects defined in YAML w/ !ruby/object" do
27
- YAML.safe_load "--- !ruby/object:ExploitableBackDoor\nfoo: bar\n"
28
- ExploitableBackDoor.should_not be_exploited
71
+ object = YAML.safe_load("--- !ruby/object:ExploitableBackDoor\nfoo: bar\n")
72
+ expect(object).not_to be_a(ExploitableBackDoor)
29
73
  end
30
74
 
31
- it "does NOT allow exploits through objects defined in YAML w/ !ruby/hash" do
32
- YAML.safe_load "--- !ruby/hash:ExploitableBackDoor\nfoo: bar\n"
33
- ExploitableBackDoor.should_not be_exploited
75
+ context "for YAML engine #{SafeYAML::YAML_ENGINE}" do
76
+ if SafeYAML::YAML_ENGINE == "psych"
77
+ let(:options) { nil }
78
+ let(:arguments) { ["foo: bar", nil, options] }
79
+
80
+ context "when no tags are whitelisted" do
81
+ it "constructs a SafeYAML::PsychHandler to resolve nodes as they're parsed, for optimal performance" do
82
+ expect(Psych::Parser).to receive(:new).with an_instance_of(SafeYAML::PsychHandler)
83
+ # This won't work now; we just want to ensure Psych::Parser#parse was in fact called.
84
+ YAML.safe_load(*arguments) rescue nil
85
+ end
86
+ end
87
+
88
+ context "when whitelisted tags are specified" do
89
+ let(:options) {
90
+ { :whitelisted_tags => ["foo"] }
91
+ }
92
+
93
+ it "instead uses Psych to construct a full tree before examining the nodes" do
94
+ expect(Psych).to receive(:parse)
95
+ # This won't work now; we just want to ensure Psych::Parser#parse was in fact called.
96
+ YAML.safe_load(*arguments) rescue nil
97
+ end
98
+ end
99
+ end
100
+
101
+ if SafeYAML::YAML_ENGINE == "syck"
102
+ it "uses Syck internally to parse YAML" do
103
+ expect(YAML).to receive(:parse).with("foo: bar")
104
+ # This won't work now; we just want to ensure YAML::parse was in fact called.
105
+ YAML.safe_load("foo: bar") rescue nil
106
+ end
107
+ end
34
108
  end
35
109
 
36
110
  it "loads a plain ol' YAML document just fine" do
37
111
  result = YAML.safe_load <<-YAML.unindent
38
112
  foo:
39
113
  number: 1
114
+ boolean: true
115
+ nil: ~
40
116
  string: Hello, there!
41
117
  symbol: :blah
42
118
  sequence:
@@ -44,14 +120,612 @@ describe YAML do
44
120
  - bye
45
121
  YAML
46
122
 
47
- result.should == {
123
+ expect(result).to eq({
48
124
  "foo" => {
49
- "number" => 1,
50
- "string" => "Hello, there!",
51
- "symbol" => :blah,
125
+ "number" => 1,
126
+ "boolean" => true,
127
+ "nil" => nil,
128
+ "string" => "Hello, there!",
129
+ "symbol" => ":blah",
52
130
  "sequence" => ["hi", "bye"]
53
131
  }
54
- }
132
+ })
133
+ end
134
+
135
+ it "works for YAML documents with anchors and aliases" do
136
+ result = YAML.safe_load <<-YAML
137
+ - &id001 {}
138
+ - *id001
139
+ - *id001
140
+ YAML
141
+
142
+ expect(result).to eq([{}, {}, {}])
143
+ end
144
+
145
+ it "works for YAML documents with binary tagged keys" do
146
+ result = YAML.safe_load <<-YAML
147
+ ? !!binary >
148
+ Zm9v
149
+ : "bar"
150
+ ? !!binary >
151
+ YmFy
152
+ : "baz"
153
+ YAML
154
+
155
+ expect(result).to eq({"foo" => "bar", "bar" => "baz"})
156
+ end
157
+
158
+ it "works for YAML documents with binary tagged values" do
159
+ result = YAML.safe_load <<-YAML
160
+ "foo": !!binary >
161
+ YmFy
162
+ "bar": !!binary >
163
+ YmF6
164
+ YAML
165
+
166
+ expect(result).to eq({"foo" => "bar", "bar" => "baz"})
167
+ end
168
+
169
+ it "works for YAML documents with binary tagged array values" do
170
+ result = YAML.safe_load <<-YAML
171
+ - !binary |-
172
+ Zm9v
173
+ - !binary |-
174
+ YmFy
175
+ YAML
176
+
177
+ expect(result).to eq(["foo", "bar"])
178
+ end
179
+
180
+ it "works for YAML documents with sections" do
181
+ result = YAML.safe_load <<-YAML
182
+ mysql: &mysql
183
+ adapter: mysql
184
+ pool: 30
185
+ login: &login
186
+ username: user
187
+ password: password123
188
+ development: &development
189
+ <<: *mysql
190
+ <<: *login
191
+ host: localhost
192
+ YAML
193
+
194
+ expect(result).to eq({
195
+ "mysql" => {
196
+ "adapter" => "mysql",
197
+ "pool" => 30
198
+ },
199
+ "login" => {
200
+ "username" => "user",
201
+ "password" => "password123"
202
+ },
203
+ "development" => {
204
+ "adapter" => "mysql",
205
+ "pool" => 30,
206
+ "username" => "user",
207
+ "password" => "password123",
208
+ "host" => "localhost"
209
+ }
210
+ })
211
+ end
212
+
213
+ it "correctly prefers explicitly defined values over default values from included sections" do
214
+ # Repeating this test 100 times to increase the likelihood of running into an issue caused by
215
+ # non-deterministic hash key enumeration.
216
+ 100.times do
217
+ result = YAML.safe_load <<-YAML
218
+ defaults: &defaults
219
+ foo: foo
220
+ bar: bar
221
+ baz: baz
222
+ custom:
223
+ <<: *defaults
224
+ bar: custom_bar
225
+ baz: custom_baz
226
+ YAML
227
+
228
+ expect(result["custom"]).to eq({
229
+ "foo" => "foo",
230
+ "bar" => "custom_bar",
231
+ "baz" => "custom_baz"
232
+ })
233
+ end
234
+ end
235
+
236
+ it "works with multi-level inheritance" do
237
+ result = YAML.safe_load <<-YAML
238
+ defaults: &defaults
239
+ foo: foo
240
+ bar: bar
241
+ baz: baz
242
+ custom: &custom
243
+ <<: *defaults
244
+ bar: custom_bar
245
+ baz: custom_baz
246
+ grandcustom: &grandcustom
247
+ <<: *custom
248
+ YAML
249
+
250
+ expect(result).to eq({
251
+ "defaults" => { "foo" => "foo", "bar" => "bar", "baz" => "baz" },
252
+ "custom" => { "foo" => "foo", "bar" => "custom_bar", "baz" => "custom_baz" },
253
+ "grandcustom" => { "foo" => "foo", "bar" => "custom_bar", "baz" => "custom_baz" }
254
+ })
255
+ end
256
+
257
+ it "returns false when parsing an empty document" do
258
+ expect([
259
+ YAML.safe_load(""),
260
+ YAML.safe_load(" "),
261
+ YAML.safe_load("\n")
262
+ ]).to eq([false, false, false])
263
+ end
264
+
265
+ it "returns nil when parsing a single value representing nil" do
266
+ expect([
267
+ YAML.safe_load("~"),
268
+ YAML.safe_load("null")
269
+ ]).to eq([nil, nil])
270
+ end
271
+
272
+ context "with custom initializers defined" do
273
+ before :each do
274
+ if SafeYAML::YAML_ENGINE == "psych"
275
+ SafeYAML::OPTIONS[:custom_initializers] = {
276
+ "!set" => lambda { Set.new },
277
+ "!hashiemash" => lambda { Hashie::Mash.new }
278
+ }
279
+ else
280
+ SafeYAML::OPTIONS[:custom_initializers] = {
281
+ "tag:yaml.org,2002:set" => lambda { Set.new },
282
+ "tag:yaml.org,2002:hashiemash" => lambda { Hashie::Mash.new }
283
+ }
284
+ end
285
+ end
286
+
287
+ it "will use a custom initializer to instantiate an array-like class upon deserialization" do
288
+ result = YAML.safe_load <<-YAML.unindent
289
+ --- !set
290
+ - 1
291
+ - 2
292
+ - 3
293
+ YAML
294
+
295
+ expect(result).to be_a(Set)
296
+ expect(result.to_a).to match_array([1, 2, 3])
297
+ end
298
+
299
+ it "will use a custom initializer to instantiate a hash-like class upon deserialization" do
300
+ result = YAML.safe_load <<-YAML.unindent
301
+ --- !hashiemash
302
+ foo: bar
303
+ YAML
304
+
305
+ expect(result).to be_a(Hashie::Mash)
306
+ expect(result.to_hash).to eq({ "foo" => "bar" })
307
+ end
308
+ end
309
+
310
+ context "with special whitelisted tags defined" do
311
+ before :each do
312
+ SafeYAML::whitelist!(OpenStruct)
313
+
314
+ # Necessary for deserializing OpenStructs properly.
315
+ SafeYAML::OPTIONS[:deserialize_symbols] = true
316
+ end
317
+
318
+ it "will allow objects to be deserialized for whitelisted tags" do
319
+ result = YAML.safe_load("--- !ruby/object:OpenStruct\ntable:\n foo: bar\n")
320
+ expect(result).to be_a(OpenStruct)
321
+ expect(result.instance_variable_get(:@table)).to eq({ "foo" => "bar" })
322
+ end
323
+
324
+ it "will not deserialize objects without whitelisted tags" do
325
+ result = YAML.safe_load("--- !ruby/hash:ExploitableBackDoor\nfoo: bar\n")
326
+ expect(result).not_to be_a(ExploitableBackDoor)
327
+ expect(result).to eq({ "foo" => "bar" })
328
+ end
329
+
330
+ it "will not allow non-whitelisted objects to be embedded within objects with whitelisted tags" do
331
+ result = YAML.safe_load <<-YAML.unindent
332
+ --- !ruby/object:OpenStruct
333
+ table:
334
+ :backdoor: !ruby/object:ExploitableBackDoor
335
+ foo: bar
336
+ YAML
337
+
338
+ expect(result).to be_a(OpenStruct)
339
+ expect(result.backdoor).not_to be_a(ExploitableBackDoor)
340
+ expect(result.backdoor).to eq({ "foo" => "bar" })
341
+ end
342
+
343
+ context "with the :raise_on_unknown_tag option enabled" do
344
+ before :each do
345
+ SafeYAML::OPTIONS[:raise_on_unknown_tag] = true
346
+ end
347
+
348
+ after :each do
349
+ SafeYAML.restore_defaults!
350
+ end
351
+
352
+ it "raises an exception if a non-nil, non-whitelisted tag is encountered" do
353
+ expect {
354
+ YAML.safe_load <<-YAML.unindent
355
+ --- !ruby/object:Unknown
356
+ foo: bar
357
+ YAML
358
+ }.to raise_error
359
+ end
360
+
361
+ it "checks all tags, even those within objects with trusted tags" do
362
+ expect {
363
+ YAML.safe_load <<-YAML.unindent
364
+ --- !ruby/object:OpenStruct
365
+ table:
366
+ :backdoor: !ruby/object:Unknown
367
+ foo: bar
368
+ YAML
369
+ }.to raise_error
370
+ end
371
+
372
+ it "does not raise an exception as long as all tags are whitelisted" do
373
+ result = YAML.safe_load <<-YAML.unindent
374
+ --- !ruby/object:OpenStruct
375
+ table:
376
+ :backdoor:
377
+ string: foo
378
+ integer: 1
379
+ float: 3.14
380
+ symbol: :bar
381
+ date: 2013-02-20
382
+ array: []
383
+ hash: {}
384
+ YAML
385
+
386
+ expect(result).to be_a(OpenStruct)
387
+ expect(result.backdoor).to eq({
388
+ "string" => "foo",
389
+ "integer" => 1,
390
+ "float" => 3.14,
391
+ "symbol" => :bar,
392
+ "date" => Date.parse("2013-02-20"),
393
+ "array" => [],
394
+ "hash" => {}
395
+ })
396
+ end
397
+
398
+ it "does not raise an exception on the non-specific '!' tag" do
399
+ result = nil
400
+ expect { result = YAML.safe_load "--- ! 'foo'" }.to_not raise_error
401
+ expect(result).to eq("foo")
402
+ end
403
+
404
+ context "with whitelisted custom class" do
405
+ class SomeClass
406
+ attr_accessor :foo
407
+ end
408
+ let(:instance) { SomeClass.new }
409
+
410
+ before do
411
+ SafeYAML::whitelist!(SomeClass)
412
+ instance.foo = 'with trailing whitespace: '
413
+ end
414
+
415
+ it "does not raise an exception on the non-specific '!' tag" do
416
+ result = nil
417
+ expect { result = YAML.safe_load(instance.to_yaml) }.to_not raise_error
418
+ expect(result.foo).to eq('with trailing whitespace: ')
419
+ end
420
+ end
421
+ end
422
+ end
423
+
424
+ context "when options are passed direclty to #load which differ from the defaults" do
425
+ let(:default_options) { {} }
426
+
427
+ before :each do
428
+ SafeYAML::OPTIONS.merge!(default_options)
429
+ end
430
+
431
+ context "(for example, when symbol deserialization is enabled by default)" do
432
+ let(:default_options) { { :deserialize_symbols => true } }
433
+
434
+ it "goes with the default option when it is not overridden" do
435
+ silence_warnings do
436
+ expect(YAML.load(":foo: bar")).to eq({ :foo => "bar" })
437
+ end
438
+ end
439
+
440
+ it "allows the default option to be overridden on a per-call basis" do
441
+ silence_warnings do
442
+ expect(YAML.load(":foo: bar", :deserialize_symbols => false)).to eq({ ":foo" => "bar" })
443
+ expect(YAML.load(":foo: bar", :deserialize_symbols => true)).to eq({ :foo => "bar" })
444
+ end
445
+ end
446
+ end
447
+
448
+ context "(or, for example, when certain tags are whitelisted)" do
449
+ let(:default_options) {
450
+ {
451
+ :deserialize_symbols => true,
452
+ :whitelisted_tags => SafeYAML::YAML_ENGINE == "psych" ?
453
+ ["!ruby/object:OpenStruct"] :
454
+ ["tag:ruby.yaml.org,2002:object:OpenStruct"]
455
+ }
456
+ }
457
+
458
+ it "goes with the default option when it is not overridden" do
459
+ result = safe_load_round_trip(OpenStruct.new(:foo => "bar"))
460
+ expect(result).to be_a(OpenStruct)
461
+ expect(result.foo).to eq("bar")
462
+ end
463
+
464
+ it "allows the default option to be overridden on a per-call basis" do
465
+ result = safe_load_round_trip(OpenStruct.new(:foo => "bar"), :whitelisted_tags => [])
466
+ expect(result).to eq({ "table" => { :foo => "bar" } })
467
+
468
+ result = safe_load_round_trip(OpenStruct.new(:foo => "bar"), :deserialize_symbols => false, :whitelisted_tags => [])
469
+ expect(result).to eq({ "table" => { ":foo" => "bar" } })
470
+ end
471
+ end
472
+ end
473
+ end
474
+
475
+ describe "unsafe_load_file" do
476
+ if SafeYAML::YAML_ENGINE == "psych" && RUBY_VERSION >= "1.9.3"
477
+ it "allows exploits through objects defined in YAML w/ !ruby/hash via custom :[]= methods" do
478
+ backdoor = YAML.unsafe_load_file "spec/exploit.1.9.3.yaml"
479
+ expect(backdoor).to be_exploited_through_setter
480
+ end
481
+ end
482
+
483
+ if SafeYAML::YAML_ENGINE == "psych" && RUBY_VERSION >= "1.9.2"
484
+ it "allows exploits through objects defined in YAML w/ !ruby/object via the :init_with method" do
485
+ backdoor = YAML.unsafe_load_file "spec/exploit.1.9.2.yaml"
486
+ expect(backdoor).to be_exploited_through_init_with
487
+ end
488
+ end
489
+
490
+ it "allows exploits through objects w/ sensitive instance variables defined in YAML w/ !ruby/object" do
491
+ backdoor = YAML.unsafe_load_file "spec/exploit.1.9.2.yaml"
492
+ expect(backdoor).to be_exploited_through_ivars
493
+ end
494
+ end
495
+
496
+ describe "safe_load_file" do
497
+ it "does NOT allow exploits through objects defined in YAML w/ !ruby/hash" do
498
+ object = YAML.safe_load_file "spec/exploit.1.9.3.yaml"
499
+ expect(object).not_to be_a(ExploitableBackDoor)
500
+ end
501
+
502
+ it "does NOT allow exploits through objects defined in YAML w/ !ruby/object" do
503
+ object = YAML.safe_load_file "spec/exploit.1.9.2.yaml"
504
+ expect(object).not_to be_a(ExploitableBackDoor)
505
+ end
506
+
507
+ it "returns false when parsing an empty file" do
508
+ expect(YAML.safe_load_file("spec/issue49.yml")).to eq(false)
509
+ end
510
+ end
511
+
512
+ describe "load" do
513
+ let(:options) { {} }
514
+
515
+ let (:arguments) {
516
+ if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
517
+ ["foo: bar", nil, options]
518
+ else
519
+ ["foo: bar", options]
520
+ end
521
+ }
522
+
523
+ context "as long as a :default_mode has been specified" do
524
+ it "doesn't issue a warning for safe mode, since an explicit mode has been set" do
525
+ SafeYAML::OPTIONS[:default_mode] = :safe
526
+ expect(Kernel).not_to receive(:warn)
527
+ YAML.load(*arguments)
528
+ end
529
+
530
+ it "doesn't issue a warning for unsafe mode, since an explicit mode has been set" do
531
+ SafeYAML::OPTIONS[:default_mode] = :unsafe
532
+ expect(Kernel).not_to receive(:warn)
533
+ YAML.load(*arguments)
534
+ end
535
+ end
536
+
537
+ context "when the :safe options is specified" do
538
+ let(:safe_mode) { true }
539
+ let(:options) { { :safe => safe_mode } }
540
+
541
+ it "doesn't issue a warning" do
542
+ expect(Kernel).not_to receive(:warn)
543
+ YAML.load(*arguments)
544
+ end
545
+
546
+ it "calls #safe_load if the :safe option is set to true" do
547
+ expect(YAML).to receive(:safe_load)
548
+ YAML.load(*arguments)
549
+ end
550
+
551
+ context "when the :safe option is set to false" do
552
+ let(:safe_mode) { false }
553
+
554
+ it "calls #unsafe_load if the :safe option is set to false" do
555
+ expect(YAML).to receive(:unsafe_load)
556
+ YAML.load(*arguments)
557
+ end
558
+ end
559
+ end
560
+
561
+ it "issues a warning when the :safe option is omitted" do
562
+ silence_warnings do
563
+ expect(Kernel).to receive(:warn)
564
+ YAML.load(*arguments)
565
+ end
566
+ end
567
+
568
+ it "only issues a warning once (to avoid spamming an app's output)" do
569
+ silence_warnings do
570
+ expect(Kernel).to receive(:warn).once
571
+ 2.times { YAML.load(*arguments) }
572
+ end
573
+ end
574
+
575
+ it "defaults to safe mode if the :safe option is omitted" do
576
+ silence_warnings do
577
+ expect(YAML).to receive(:safe_load)
578
+ YAML.load(*arguments)
579
+ end
580
+ end
581
+
582
+ context "with the default mode set to :unsafe" do
583
+ before :each do
584
+ SafeYAML::OPTIONS[:default_mode] = :unsafe
585
+ end
586
+
587
+ it "defaults to unsafe mode if the :safe option is omitted" do
588
+ silence_warnings do
589
+ expect(YAML).to receive(:unsafe_load)
590
+ YAML.load(*arguments)
591
+ end
592
+ end
593
+
594
+ it "calls #safe_load if the :safe option is set to true" do
595
+ expect(YAML).to receive(:safe_load)
596
+ YAML.load(*(arguments + [{ :safe => true }]))
597
+ end
598
+ end
599
+ end
600
+
601
+ describe "load_file" do
602
+ let(:filename) { "spec/exploit.1.9.2.yaml" } # doesn't really matter
603
+
604
+ it "issues a warning if the :safe option is omitted" do
605
+ silence_warnings do
606
+ expect(Kernel).to receive(:warn)
607
+ YAML.load_file(filename)
608
+ end
609
+ end
610
+
611
+ it "doesn't issue a warning as long as the :safe option is specified" do
612
+ expect(Kernel).not_to receive(:warn)
613
+ YAML.load_file(filename, :safe => true)
614
+ end
615
+
616
+ it "defaults to safe mode if the :safe option is omitted" do
617
+ silence_warnings do
618
+ expect(YAML).to receive(:safe_load_file)
619
+ YAML.load_file(filename)
620
+ end
621
+ end
622
+
623
+ it "calls #safe_load_file if the :safe option is set to true" do
624
+ expect(YAML).to receive(:safe_load_file)
625
+ YAML.load_file(filename, :safe => true)
626
+ end
627
+
628
+ it "calls #unsafe_load_file if the :safe option is set to false" do
629
+ expect(YAML).to receive(:unsafe_load_file)
630
+ YAML.load_file(filename, :safe => false)
631
+ end
632
+
633
+ context "with arbitrary object deserialization enabled by default" do
634
+ before :each do
635
+ SafeYAML::OPTIONS[:default_mode] = :unsafe
636
+ end
637
+
638
+ it "defaults to unsafe mode if the :safe option is omitted" do
639
+ silence_warnings do
640
+ expect(YAML).to receive(:unsafe_load_file)
641
+ YAML.load_file(filename)
642
+ end
643
+ end
644
+
645
+ it "calls #safe_load if the :safe option is set to true" do
646
+ expect(YAML).to receive(:safe_load_file)
647
+ YAML.load_file(filename, :safe => true)
648
+ end
649
+ end
650
+
651
+ it "handles files starting with --- (see issue #48)" do
652
+ expect(YAML.load_file("spec/issue48.txt", :safe => true)).to eq({
653
+ "title" => "Blah",
654
+ "key" => "value"
655
+ })
656
+ end
657
+
658
+ it "handles content starting with --- (see issue #48)" do
659
+ yaml = File.read("spec/issue48.txt")
660
+ expect(YAML.load(yaml, :safe => true)).to eq({
661
+ "title" => "Blah",
662
+ "key" => "value"
663
+ })
664
+ end
665
+ end
666
+
667
+ describe "whitelist!" do
668
+ context "not a class" do
669
+ it "should raise" do
670
+ expect { SafeYAML::whitelist! :foo }.to raise_error(/not a Class/)
671
+ expect(SafeYAML::OPTIONS[:whitelisted_tags]).to be_empty
672
+ end
673
+ end
674
+
675
+ context "anonymous class" do
676
+ it "should raise" do
677
+ expect { SafeYAML::whitelist! Class.new }.to raise_error(/cannot be anonymous/)
678
+ expect(SafeYAML::OPTIONS[:whitelisted_tags]).to be_empty
679
+ end
680
+ end
681
+
682
+ context "with a Class as its argument" do
683
+ it "should configure correctly" do
684
+ expect { SafeYAML::whitelist! OpenStruct }.to_not raise_error
685
+ expect(SafeYAML::OPTIONS[:whitelisted_tags].grep(/OpenStruct\Z/)).not_to be_empty
686
+ end
687
+
688
+ it "successfully deserializes the specified class" do
689
+ SafeYAML.whitelist!(OpenStruct)
690
+
691
+ # necessary for properly assigning OpenStruct attributes
692
+ SafeYAML::OPTIONS[:deserialize_symbols] = true
693
+
694
+ result = safe_load_round_trip(OpenStruct.new(:foo => "bar"))
695
+ expect(result).to be_a(OpenStruct)
696
+ expect(result.foo).to eq("bar")
697
+ end
698
+
699
+ it "works for ranges" do
700
+ SafeYAML.whitelist!(Range)
701
+ expect(safe_load_round_trip(1..10)).to eq(1..10)
702
+ end
703
+
704
+ it "works for regular expressions" do
705
+ SafeYAML.whitelist!(Regexp)
706
+ expect(safe_load_round_trip(/foo/)).to eq(/foo/)
707
+ end
708
+
709
+ it "works for multiple classes" do
710
+ SafeYAML.whitelist!(Range, Regexp)
711
+ expect(safe_load_round_trip([(1..10), /bar/])).to eq([(1..10), /bar/])
712
+ end
713
+
714
+ it "works for arbitrary Exception subclasses" do
715
+ class CustomException < Exception
716
+ attr_reader :custom_message
717
+
718
+ def initialize(custom_message)
719
+ @custom_message = custom_message
720
+ end
721
+ end
722
+
723
+ SafeYAML.whitelist!(CustomException)
724
+
725
+ ex = safe_load_round_trip(CustomException.new("blah"))
726
+ expect(ex).to be_a(CustomException)
727
+ expect(ex.custom_message).to eq("blah")
728
+ end
55
729
  end
56
730
  end
57
731
  end