safe_yaml 0.8.6 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -3,7 +3,9 @@ SafeYAML
3
3
 
4
4
  [![Build Status](https://travis-ci.org/dtao/safe_yaml.png)](http://travis-ci.org/dtao/safe_yaml)
5
5
 
6
- The **SafeYAML** gem provides an alternative implementation of `YAML.load` suitable for accepting user input in Ruby applications. Unlike Ruby's built-in implementation of `YAML.load`, SafeYAML's version will not expose apps to arbitrary code execution exploits (such as [the ones recently](http://www.reddit.com/r/netsec/comments/167c11/serious_vulnerability_in_ruby_on_rails_allowing/) [discovered in Rails](http://www.h-online.com/open/news/item/Rails-developers-close-another-extremely-critical-flaw-1793511.html)).
6
+ The **SafeYAML** gem provides an alternative implementation of `YAML.load` suitable for accepting user input in Ruby applications. Unlike Ruby's built-in implementation of `YAML.load`, SafeYAML's version will not expose apps to arbitrary code execution exploits (such as [the ones discovered](http://www.reddit.com/r/netsec/comments/167c11/serious_vulnerability_in_ruby_on_rails_allowing/) [in Rails in early 2013](http://www.h-online.com/open/news/item/Rails-developers-close-another-extremely-critical-flaw-1793511.html)).
7
+
8
+ **If you encounter any issues with SafeYAML, check out the 'Common Issues' section below.** If you don't see anything that addresses the problem you're experiencing, by all means, [create an issue](https://github.com/dtao/safe_yaml/issues/new)!
7
9
 
8
10
  Installation
9
11
  ------------
@@ -20,13 +22,23 @@ Or install it yourself as:
20
22
 
21
23
  $ gem install safe_yaml
22
24
 
23
- Purpose
24
- -------
25
+ Configuration
26
+ -------------
27
+
28
+ Configuring SafeYAML should be quick. In most cases, you will probably only have to think about two things:
29
+
30
+ 1. What do you want the `YAML` module's *default* behavior to be? Set the `SafeYAML::OPTIONS[:default_mode]` option to either `:safe` or `:unsafe` to control this. If you do neither, SafeYAML will default to `:safe` mode but will issue a warning the first time you call `YAML.load`.
31
+ 2. Do you want to allow symbols by default? Set the `SafeYAML::OPTIONS[:deserialize_symbols]` option to `true` or `false` to control this. The default is `false`, which means that SafeYAML will deserialize symbols in YAML documents as strings.
25
32
 
26
- Suppose your application were to contain some code like this:
33
+ For more information on these and other options, see the "Usage" section down below.
34
+
35
+ Explanation
36
+ -----------
37
+
38
+ Suppose your application were to use a popular open source library which contained code like this:
27
39
 
28
40
  ```ruby
29
- class ExploitableClassBuilder
41
+ class ClassBuilder
30
42
  def []=(key, value)
31
43
  @class ||= Class.new
32
44
 
@@ -43,50 +55,47 @@ class ExploitableClassBuilder
43
55
  end
44
56
  ```
45
57
 
46
- Now, if you were to use `YAML.load` on user input anywhere in your application without the SafeYAML gem installed, an attacker could make a request with a carefully-crafted YAML string to execute arbitrary code (yes, including `system("unix command")`) on your servers.
58
+ Now, if you were to use `YAML.load` on user input anywhere in your application without the SafeYAML gem installed, an attacker who suspected you were using this library could send a request with a carefully-crafted YAML string to execute arbitrary code (yes, including `system("unix command")`) on your servers.
47
59
 
48
- Observe:
60
+ This simple example demonstrates the vulnerability:
49
61
 
50
62
  ```ruby
51
63
  yaml = <<-EOYAML
52
- --- !ruby/hash:ExploitableClassBuilder
64
+ --- !ruby/hash:ClassBuilder
53
65
  "foo; end; puts %(I'm in yr system!); def bar": "baz"
54
66
  EOYAML
55
67
  ```
56
68
 
57
69
  > YAML.load(yaml)
58
70
  I'm in yr system!
59
- => #<ExploitableClassBuilder:0x007fdbbe2e25d8 @class=#<Class:0x007fdbbe2e2510>>
71
+ => #<ClassBuilder:0x007fdbbe2e25d8 @class=#<Class:0x007fdbbe2e2510>>
60
72
 
61
- With SafeYAML, that attacker would be thwarted:
73
+ With SafeYAML, the same attacker would be thwarted:
62
74
 
63
75
  > require "safe_yaml"
64
76
  => true
65
- > YAML.safe_load(yaml)
77
+ > YAML.load(yaml, :safe => true)
66
78
  => {"foo; end; puts %(I'm in yr system!); def bar"=>"baz"}
67
79
 
68
80
  Usage
69
81
  -----
70
82
 
71
- `YAML.safe_load` will load YAML without allowing arbitrary object deserialization.
83
+ When you require the safe_yaml gem in your project, `YAML.load` is patched to accept one additional (optional) `options` parameter. This changes the method signature as follows:
72
84
 
73
- `YAML.unsafe_load` will exhibit Ruby's built-in behavior: to allow the deserialization of arbitrary objects.
85
+ - for Syck and Psych prior to Ruby 1.9.3: `YAML.load(yaml, options={})`
86
+ - for Psych in 1.9.3 and later: `YAML.load(yaml, filename=nil, options={})`
74
87
 
75
- By default, when you require the safe_yaml gem in your project, `YAML.load` is patched to internally call `safe_load`. The patched method also accepts a `:safe` flag to specify which version to use:
88
+ The most important option is the `:safe` option (default: `true`), which controls whether or not to deserialize arbitrary objects when parsing a YAML document. The other options, along with explanations, are as follows.
76
89
 
77
- ```ruby
78
- # Ruby >= 1.9.3
79
- YAML.load(yaml, filename, :safe => true) # calls safe_load
80
- YAML.load(yaml, filename, :safe => false) # calls unsafe_load
90
+ - `:deserialize_symbols` (default: `false`): Controls whether or not YAML will deserialize symbols. It is probably best to only enable this option where necessary, e.g. to make trusted libraries work. Symbols receive special treatment in Ruby and are not garbage collected, which means deserializing them indiscriminately may render your site vulnerable to a DOS attack (hence `false` as a default value).
81
91
 
82
- # Ruby < 1.9.3
83
- YAML.load(yaml, :safe => true) # calls safe_load
84
- YAML.load(yaml, :safe => false) # calls unsafe_load
85
- ```
92
+ - `:whitelisted_tags`: Accepts an array of YAML tags that designate trusted types, e.g., ones that can be deserialized without worrying about any resulting security vulnerabilities. When any of the given tags are encountered in a YAML document, the associated data will be parsed by the underlying YAML engine (Syck or Psych) for the version of Ruby you are using. See the "Whitelisting Trusted Types" section below for more information.
93
+
94
+ - `:custom_initializers`: Similar to the `:whitelisted_tags` option, but allows you to provide your own initializers for specified tags rather than using Syck or Psyck. Accepts a hash with string tags for keys and lambdas for values.
86
95
 
87
- The default behavior can be switched to unsafe loading by calling `SafeYAML::OPTIONS[:default_mode] = :unsafe`. In this case, the `:safe` flag still has the same effect, but the defaults are reversed (so calling `YAML.load` will have the same behavior as if the safe_yaml gem weren't required).
96
+ - `:raise_on_unknown_tag` (default: `false`): Represents the highest possible level of paranoia (not necessarily a bad thing); if the YAML engine encounters any tag other than ones that are automatically trusted by SafeYAML or that you've explicitly whitelisted, it will raise an exception. This may be a good choice if you expect to always be dealing with perfectly safe YAML and want your application to fail loudly upon encountering questionable data.
88
97
 
89
- This gem will also warn you whenever you use `YAML.load` without specifying the `:safe` option, or if you have not explicitly specified a default mode using the `:default_mode` option.
98
+ All of the above options can be set at the global level via `SafeYAML::OPTIONS`. You can also set each one individually per call to `YAML.load`; an option explicitly passed to `load` will take precedence over an option specified globally.
90
99
 
91
100
  Supported Types
92
101
  ---------------
@@ -102,30 +111,36 @@ The way that SafeYAML works is by restricting the kinds of objects that can be d
102
111
  - Booleans
103
112
  - Nils
104
113
 
105
- Deserialization of symbols can also be enabled by setting `SafeYAML::OPTIONS[:deserialize_symbols] = true` (for example, in an initializer). Be aware, however, that symbols in Ruby are not garbage-collected; therefore enabling symbol deserialization in your application may leave you vulnerable to [DOS attacks](http://en.wikipedia.org/wiki/Denial-of-service_attack).
114
+ Again, deserialization of symbols can be enabled globally by setting `SafeYAML::OPTIONS[:deserialize_symbols] = true`, or in a specific call to `YAML.load([some yaml], :deserialize_symbols => true)`.
106
115
 
107
116
  Whitelisting Trusted Types
108
117
  --------------------------
109
118
 
110
- SafeYAML now also supports **whitelisting** certain YAML tags for trusted types. This is handy when your application may use YAML to serialize and deserialize certain types not listed above, which you know to be free of any deserialization-related vulnerabilities. You can whitelist tags via the `:whitelisted_tags` option:
119
+ SafeYAML supports whitelisting certain YAML tags for trusted types. This is handy when your application uses YAML to serialize and deserialize certain types not listed above, which you know to be free of any deserialization-related vulnerabilities.
120
+
121
+ The easiest way to whitelist types is by calling `SafeYAML.whitelist!`, which can accept a variable number of safe types, e.g.:
122
+
123
+ ```ruby
124
+ SafeYAML.whitelist!(FrobDispenser, GobbleFactory)
125
+ ```
126
+
127
+ You can also whitelist YAML *tags* via the `:whitelisted_tags` option:
111
128
 
112
129
  ```ruby
113
- # Using Syck (unfortunately, Syck and Psych use different tagging schemes)
130
+ # Using Syck
114
131
  SafeYAML::OPTIONS[:whitelisted_tags] = ["tag:ruby.yaml.org,2002:object:OpenStruct"]
115
132
 
116
133
  # Using Psych
117
134
  SafeYAML::OPTIONS[:whitelisted_tags] = ["!ruby/object:OpenStruct"]
118
135
  ```
119
136
 
120
- When SafeYAML encounters whitelisted tags in a YAML document, it will default to the deserialization capabilities of the underlying YAML engine (i.e., either Syck or Psych).
121
-
122
- **However**, this feature will *not* allow would-be attackers to embed untrusted types within trusted types:
137
+ And in case you were wondering: no, this feature will *not* allow would-be attackers to embed untrusted types within trusted types:
123
138
 
124
139
  ```ruby
125
140
  yaml = <<-EOYAML
126
141
  --- !ruby/object:OpenStruct
127
142
  table:
128
- :backdoor: !ruby/hash:ExploitableClassBuilder
143
+ :backdoor: !ruby/hash:ClassBuilder
129
144
  "foo; end; puts %(I'm in yr system!); def bar": "baz"
130
145
  EOYAML
131
146
  ```
@@ -133,22 +148,17 @@ EOYAML
133
148
  > YAML.safe_load(yaml)
134
149
  => #<OpenStruct :backdoor={"foo; end; puts %(I'm in yr system!); def bar"=>"baz"}>
135
150
 
136
- You may prefer, rather than quietly sanitizing and accepting YAML documents with unknown tags, to fail loudly when questionable data is encountered. In this case, you can also set the `:raise_on_unknown_tag` option to `true`:
137
-
138
- ```ruby
139
- SafeYAML::OPTIONS[:raise_on_unknown_tag] = true
140
- ```
141
-
142
- > YAML.safe_load(yaml)
143
- => RuntimeError: Unknown YAML tag '!ruby/hash:ExploitableClassBuilder'
144
-
145
- Pretty sweet, right?
146
-
147
151
  Known Issues
148
152
  ------------
149
153
 
150
- Be aware that some Ruby libraries, particularly those requiring inter-process communication, leverage YAML's object deserialization functionality and therefore may break or otherwise be impacted by SafeYAML. The following list includes known instances of SafeYAML's interaction with other Ruby gems:
154
+ If you add SafeYAML to your project and start seeing any errors about missing keys, or you notice mysterious strings that look like `":foo"` (i.e., start with a colon), it's likely you're seeing errors from symbols being saved in YAML format. If you are able to modify the offending code, you might want to consider changing your YAML content to use plain vanilla strings instead of symbols. If not, you may need to set the `:deserialize_symbols` option to `true`, either in calls to `YAML.load` or--as a last resort--globally, with `SafeYAML::OPTIONS[:deserialize_symbols]`.
155
+
156
+ Also be aware that some Ruby libraries, particularly those requiring inter-process communication, leverage YAML's object deserialization functionality and therefore may break or otherwise be impacted by SafeYAML. The following list includes known instances of SafeYAML's interaction with other Ruby gems:
151
157
 
158
+ - [**ActiveRecord**](https://github.com/rails/rails/tree/master/activerecord): uses YAML to control serialization of model objects using the `serialize` class method. If you find that accessing serialized properties on your ActiveRecord models is causing errors, chances are you may need to:
159
+ 1. set the `:deserialize_symbols` option to `true`,
160
+ 2. whitelist some of the types in your serialized data via `SafeYAML.whitelist!` or the `:whitelisted_tags` option, or
161
+ 3. both
152
162
  - [**Guard**](https://github.com/guard/guard): Uses YAML as a serialization format for notifications. The data serialized uses symbolic keys, so setting `SafeYAML::OPTIONS[:deserialize_symbols] = true` is necessary to allow Guard to work.
153
163
  - [**sidekiq**](https://github.com/mperham/sidekiq): Uses a YAML configiuration file with symbolic keys, so setting `SafeYAML::OPTIONS[:deserialize_symbols] = true` should allow it to work.
154
164
 
@@ -157,7 +167,9 @@ The above list will grow over time, as more issues are discovered.
157
167
  Caveat
158
168
  ------
159
169
 
160
- This gem is quite young, and so the API may (read: *will*) change in future versions. The goal of the gem is to make it as easy as possible to protect existing applications from object deserialization exploits. Any and all feedback is more than welcome.
170
+ My intention is to eventually adopt [semantic versioning](http://semver.org/) with this gem, if it ever gets to version 1.0 (i.e., doesn't become obsolete by then). Since it isn't there yet, that means that API may well change from one version to the next. Please keep that in mind if you are using it in your application.
171
+
172
+ To be clear: my *goal* is for SafeYAML to make it as easy as possible to protect existing applications from object deserialization exploits. Any and all feedback is more than welcome!
161
173
 
162
174
  Requirements
163
175
  ------------
data/lib/safe_yaml.rb CHANGED
@@ -10,46 +10,109 @@ require "safe_yaml/transform/to_nil"
10
10
  require "safe_yaml/transform/to_symbol"
11
11
  require "safe_yaml/transform"
12
12
  require "safe_yaml/resolver"
13
+ require "safe_yaml/deep"
13
14
 
14
15
  module SafeYAML
15
16
  MULTI_ARGUMENT_YAML_LOAD = YAML.method(:load).arity != 1
16
17
  YAML_ENGINE = defined?(YAML::ENGINE) ? YAML::ENGINE.yamler : "syck"
17
18
 
18
- DEFAULT_OPTIONS = {
19
+ DEFAULT_OPTIONS = Deep.freeze({
19
20
  :default_mode => nil,
20
21
  :suppress_warnings => false,
21
22
  :deserialize_symbols => false,
22
23
  :whitelisted_tags => [],
23
24
  :custom_initializers => {},
24
25
  :raise_on_unknown_tag => false
25
- }.freeze
26
+ })
26
27
 
27
- OPTIONS = DEFAULT_OPTIONS.dup
28
+ OPTIONS = Deep.copy(DEFAULT_OPTIONS)
28
29
 
29
30
  module_function
30
31
  def restore_defaults!
31
- OPTIONS.clear.merge!(DEFAULT_OPTIONS)
32
+ OPTIONS.clear.merge!(Deep.copy(DEFAULT_OPTIONS))
32
33
  end
33
34
 
34
- def tag_safety_check!(tag)
35
+ def tag_safety_check!(tag, options)
35
36
  return if tag.nil?
36
- if OPTIONS[:raise_on_unknown_tag] && !OPTIONS[:whitelisted_tags].include?(tag) && !tag_is_explicitly_trusted?(tag)
37
+ if options[:raise_on_unknown_tag] && !options[:whitelisted_tags].include?(tag) && !tag_is_explicitly_trusted?(tag)
37
38
  raise "Unknown YAML tag '#{tag}'"
38
39
  end
39
40
  end
40
41
 
42
+ def whitelist!(*classes)
43
+ classes.each do |klass|
44
+ whitelist_class!(klass)
45
+ end
46
+ end
47
+
48
+ def whitelist_class!(klass)
49
+ raise "#{klass} not a Class" unless klass.is_a?(::Class)
50
+
51
+ klass_name = klass.name
52
+ raise "#{klass} cannot be anonymous" if klass_name.nil? || klass_name.empty?
53
+
54
+ # Whitelist any built-in YAML tags supplied by Syck or Psych.
55
+ predefined_tag = predefined_tags[klass]
56
+ if predefined_tag
57
+ OPTIONS[:whitelisted_tags] << predefined_tag
58
+ return
59
+ end
60
+
61
+ # Exception is exceptional (har har).
62
+ tag_class = klass < Exception ? "exception" : "object"
63
+
64
+ tag_prefix = case YAML_ENGINE
65
+ when "psych" then "!ruby/#{tag_class}"
66
+ when "syck" then "tag:ruby.yaml.org,2002:#{tag_class}"
67
+ else raise "unknown YAML_ENGINE #{YAML_ENGINE}"
68
+ end
69
+ OPTIONS[:whitelisted_tags] << "#{tag_prefix}:#{klass_name}"
70
+ end
71
+
72
+ def predefined_tags
73
+ if @predefined_tags.nil?
74
+ @predefined_tags = {}
75
+
76
+ if YAML_ENGINE == "syck"
77
+ YAML.tagged_classes.each do |tag, klass|
78
+ @predefined_tags[klass] = tag
79
+ end
80
+
81
+ else
82
+ # Special tags appear to be hard-coded in Psych:
83
+ # https://github.com/tenderlove/psych/blob/v1.3.4/lib/psych/visitors/to_ruby.rb
84
+ # Fortunately, there aren't many that SafeYAML doesn't already support.
85
+ @predefined_tags.merge!({
86
+ Exception => "!ruby/exception",
87
+ Range => "!ruby/range",
88
+ Regexp => "!ruby/regexp",
89
+ })
90
+ end
91
+ end
92
+
93
+ @predefined_tags
94
+ end
95
+
41
96
  if YAML_ENGINE == "psych"
42
97
  def tag_is_explicitly_trusted?(tag)
43
98
  false
44
99
  end
45
100
 
46
101
  else
47
- TRUSTED_TAGS = [
48
- "tag:yaml.org,2002:str",
49
- "tag:yaml.org,2002:int",
102
+ TRUSTED_TAGS = Set.new([
103
+ "tag:yaml.org,2002:binary",
104
+ "tag:yaml.org,2002:bool#no",
105
+ "tag:yaml.org,2002:bool#yes",
106
+ "tag:yaml.org,2002:float",
50
107
  "tag:yaml.org,2002:float#fix",
108
+ "tag:yaml.org,2002:int",
109
+ "tag:yaml.org,2002:map",
110
+ "tag:yaml.org,2002:null",
111
+ "tag:yaml.org,2002:seq",
112
+ "tag:yaml.org,2002:str",
113
+ "tag:yaml.org,2002:timestamp",
51
114
  "tag:yaml.org,2002:timestamp#ymd"
52
- ].freeze
115
+ ]).freeze
53
116
 
54
117
  def tag_is_explicitly_trusted?(tag)
55
118
  TRUSTED_TAGS.include?(tag)
@@ -58,40 +121,62 @@ module SafeYAML
58
121
  end
59
122
 
60
123
  module YAML
61
- def self.load_with_options(yaml, *filename_and_options)
62
- options = filename_and_options.last || {}
124
+ def self.load_with_options(yaml, *original_arguments)
125
+ filename, options = filename_and_options_from_arguments(original_arguments)
63
126
  safe_mode = safe_mode_from_options("load", options)
64
127
  arguments = [yaml]
65
- arguments << filename_and_options.first if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
66
- safe_mode == :safe ? safe_load(*arguments) : unsafe_load(*arguments)
128
+
129
+ if safe_mode == :safe
130
+ arguments << filename if SafeYAML::YAML_ENGINE == "psych"
131
+ arguments << options_for_safe_load(options)
132
+ safe_load(*arguments)
133
+ else
134
+ arguments << filename if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
135
+ unsafe_load(*arguments)
136
+ end
67
137
  end
68
138
 
69
139
  def self.load_file_with_options(file, options={})
70
140
  safe_mode = safe_mode_from_options("load_file", options)
71
- safe_mode == :safe ? safe_load_file(file) : unsafe_load_file(file)
141
+ if safe_mode == :safe
142
+ safe_load_file(file, options_for_safe_load(options))
143
+ else
144
+ unsafe_load_file(file)
145
+ end
72
146
  end
73
147
 
74
148
  if SafeYAML::YAML_ENGINE == "psych"
75
- require "safe_yaml/safe_to_ruby_visitor"
149
+ require "safe_yaml/psych_handler"
76
150
  require "safe_yaml/psych_resolver"
77
- def self.safe_load(yaml, filename=nil)
78
- safe_resolver = SafeYAML::PsychResolver.new
79
- tree = if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
80
- Psych.parse(yaml, filename)
151
+ require "safe_yaml/safe_to_ruby_visitor"
152
+
153
+ def self.safe_load(yaml, filename=nil, options={})
154
+ # If the user hasn't whitelisted any tags, we can go with this implementation which is
155
+ # significantly faster.
156
+ if (options && options[:whitelisted_tags] || SafeYAML::OPTIONS[:whitelisted_tags]).empty?
157
+ safe_handler = SafeYAML::PsychHandler.new(options)
158
+ arguments_for_parse = [yaml]
159
+ arguments_for_parse << filename if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
160
+ Psych::Parser.new(safe_handler).parse(*arguments_for_parse)
161
+ return safe_handler.result || false
162
+
81
163
  else
82
- Psych.parse(yaml)
164
+ safe_resolver = SafeYAML::PsychResolver.new(options)
165
+ tree = SafeYAML::MULTI_ARGUMENT_YAML_LOAD ?
166
+ Psych.parse(yaml, filename) :
167
+ Psych.parse(yaml)
168
+ return safe_resolver.resolve_node(tree)
83
169
  end
84
- return safe_resolver.resolve_node(tree)
85
170
  end
86
171
 
87
- def self.safe_load_file(filename)
88
- File.open(filename, 'r:bom|utf-8') { |f| self.safe_load f, filename }
172
+ def self.safe_load_file(filename, options={})
173
+ File.open(filename, 'r:bom|utf-8') { |f| self.safe_load(f, filename, options) }
89
174
  end
90
175
 
91
176
  def self.unsafe_load_file(filename)
92
177
  if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
93
178
  # https://github.com/tenderlove/psych/blob/v1.3.2/lib/psych.rb#L296-298
94
- File.open(filename, 'r:bom|utf-8') { |f| self.unsafe_load f, filename }
179
+ File.open(filename, 'r:bom|utf-8') { |f| self.unsafe_load(f, filename) }
95
180
  else
96
181
  # https://github.com/tenderlove/psych/blob/v1.2.2/lib/psych.rb#L231-233
97
182
  self.unsafe_load File.open(filename)
@@ -102,19 +187,19 @@ module YAML
102
187
  require "safe_yaml/syck_resolver"
103
188
  require "safe_yaml/syck_node_monkeypatch"
104
189
 
105
- def self.safe_load(yaml)
106
- resolver = SafeYAML::SyckResolver.new
190
+ def self.safe_load(yaml, options={})
191
+ resolver = SafeYAML::SyckResolver.new(SafeYAML::OPTIONS.merge(options || {}))
107
192
  tree = YAML.parse(yaml)
108
193
  return resolver.resolve_node(tree)
109
194
  end
110
195
 
111
- def self.safe_load_file(filename)
112
- File.open(filename) { |f| self.safe_load f }
196
+ def self.safe_load_file(filename, options={})
197
+ File.open(filename) { |f| self.safe_load(f, options) }
113
198
  end
114
199
 
115
200
  def self.unsafe_load_file(filename)
116
201
  # https://github.com/indeyets/syck/blob/master/ext/ruby/lib/yaml.rb#L133-135
117
- File.open(filename) { |f| self.unsafe_load f }
202
+ File.open(filename) { |f| self.unsafe_load(f) }
118
203
  end
119
204
  end
120
205
 
@@ -123,37 +208,20 @@ module YAML
123
208
  alias_method :load, :load_with_options
124
209
  alias_method :load_file, :load_file_with_options
125
210
 
126
- def enable_symbol_parsing?
127
- warn_of_deprecated_method("set the SafeYAML::OPTIONS[:deserialize_symbols] option instead")
128
- SafeYAML::OPTIONS[:deserialize_symbols]
129
- end
130
-
131
- def enable_symbol_parsing!
132
- warn_of_deprecated_method("set the SafeYAML::OPTIONS[:deserialize_symbols] option instead")
133
- SafeYAML::OPTIONS[:deserialize_symbols] = true
134
- end
135
-
136
- def disable_symbol_parsing!
137
- warn_of_deprecated_method("set the SafeYAML::OPTIONS[:deserialize_symbols] option instead")
138
- SafeYAML::OPTIONS[:deserialize_symbols] = false
139
- end
140
-
141
- def enable_arbitrary_object_deserialization?
142
- warn_of_deprecated_method("set the SafeYAML::OPTIONS[:default_mode] to either :safe or :unsafe")
143
- SafeYAML::OPTIONS[:default_mode] == :unsafe
144
- end
145
-
146
- def enable_arbitrary_object_deserialization!
147
- warn_of_deprecated_method("set the SafeYAML::OPTIONS[:default_mode] to either :safe or :unsafe")
148
- SafeYAML::OPTIONS[:default_mode] = :unsafe
149
- end
211
+ private
212
+ def filename_and_options_from_arguments(arguments)
213
+ if arguments.count == 1
214
+ if arguments.first.is_a?(String)
215
+ return arguments.first, {}
216
+ else
217
+ return nil, arguments.first || {}
218
+ end
150
219
 
151
- def disable_arbitrary_object_deserialization!
152
- warn_of_deprecated_method("set the SafeYAML::OPTIONS[:default_mode] to either :safe or :unsafe")
153
- SafeYAML::OPTIONS[:default_mode] = :safe
220
+ else
221
+ return arguments.first, arguments.last || {}
222
+ end
154
223
  end
155
224
 
156
- private
157
225
  def safe_mode_from_options(method, options={})
158
226
  if options[:safe].nil?
159
227
  safe_mode = SafeYAML::OPTIONS[:default_mode] || :safe
@@ -167,9 +235,10 @@ module YAML
167
235
  options[:safe] ? :safe : :unsafe
168
236
  end
169
237
 
170
- def warn_of_deprecated_method(message)
171
- method = caller.first[/`([^']*)'$/, 1]
172
- Kernel.warn("The method 'YAML.#{method}' is deprecated and will be removed in the next release of SafeYAML -- #{message}.")
238
+ def options_for_safe_load(base_options)
239
+ options = base_options.dup
240
+ options.delete(:safe)
241
+ options
173
242
  end
174
243
  end
175
244
  end
@@ -0,0 +1,34 @@
1
+ module SafeYAML
2
+ class Deep
3
+ def self.freeze(object)
4
+ object.each do |*entry|
5
+ value = entry.last
6
+ case value
7
+ when String, Regexp
8
+ value.freeze
9
+ when Enumerable
10
+ Deep.freeze(value)
11
+ end
12
+ end
13
+
14
+ return object.freeze
15
+ end
16
+
17
+ def self.copy(object)
18
+ duplicate = object.dup rescue object
19
+
20
+ case object
21
+ when Array
22
+ (0...duplicate.count).each do |i|
23
+ duplicate[i] = Deep.copy(duplicate[i])
24
+ end
25
+ when Hash
26
+ duplicate.keys.each do |key|
27
+ duplicate[key] = Deep.copy(duplicate[key])
28
+ end
29
+ end
30
+
31
+ duplicate
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,92 @@
1
+ require "psych"
2
+ require "base64"
3
+
4
+ module SafeYAML
5
+ class PsychHandler < Psych::Handler
6
+ def initialize(options)
7
+ @options = SafeYAML::OPTIONS.merge(options || {})
8
+ @initializers = @options[:custom_initializers] || {}
9
+ @anchors = {}
10
+ @stack = []
11
+ @current_key = nil
12
+ @result = nil
13
+ end
14
+
15
+ def result
16
+ @result
17
+ end
18
+
19
+ def add_to_current_structure(value, anchor=nil, quoted=nil, tag=nil)
20
+ value = Transform.to_proper_type(value, quoted, tag, @options)
21
+
22
+ @anchors[anchor] = value if anchor
23
+
24
+ if @result.nil?
25
+ @result = value
26
+ @current_structure = @result
27
+ return
28
+ end
29
+
30
+ if @current_structure.respond_to?(:<<)
31
+ @current_structure << value
32
+
33
+ elsif @current_structure.respond_to?(:[]=)
34
+ if @current_key.nil?
35
+ @current_key = value
36
+
37
+ else
38
+ if @current_key == "<<"
39
+ @current_structure.merge!(value)
40
+ else
41
+ @current_structure[@current_key] = value
42
+ end
43
+
44
+ @current_key = nil
45
+ end
46
+
47
+ else
48
+ raise "Don't know how to add to a #{@current_structure.class}!"
49
+ end
50
+ end
51
+
52
+ def end_current_structure
53
+ @stack.pop
54
+ @current_structure = @stack.last
55
+ end
56
+
57
+ def streaming?
58
+ false
59
+ end
60
+
61
+ # event handlers
62
+ def alias(anchor)
63
+ add_to_current_structure(@anchors[anchor])
64
+ end
65
+
66
+ def scalar(value, anchor, tag, plain, quoted, style)
67
+ add_to_current_structure(value, anchor, quoted, tag)
68
+ end
69
+
70
+ def start_mapping(anchor, tag, implicit, style)
71
+ map = @initializers.include?(tag) ? @initializers[tag].call : {}
72
+ self.add_to_current_structure(map, anchor)
73
+ @current_structure = map
74
+ @stack.push(map)
75
+ end
76
+
77
+ def end_mapping
78
+ self.end_current_structure()
79
+ end
80
+
81
+ def start_sequence(anchor, tag, implicit, style)
82
+ seq = @initializers.include?(tag) ? @initializers[tag].call : []
83
+ self.add_to_current_structure(seq, anchor)
84
+ @current_structure = seq
85
+ @stack.push(seq)
86
+ end
87
+
88
+ def end_sequence
89
+ self.end_current_structure()
90
+ end
91
+ end
92
+ end
@@ -8,8 +8,8 @@ module SafeYAML
8
8
  Psych::Nodes::Alias => :alias
9
9
  }.freeze
10
10
 
11
- def initialize
12
- super()
11
+ def initialize(options={})
12
+ super
13
13
  @aliased_nodes = {}
14
14
  end
15
15
 
@@ -1,16 +1,16 @@
1
1
  module SafeYAML
2
2
  class Resolver
3
- def initialize
4
- @whitelist = SafeYAML::OPTIONS[:whitelisted_tags] || []
5
- @initializers = SafeYAML::OPTIONS[:custom_initializers] || {}
6
- @raise_on_unknown_tag = SafeYAML::OPTIONS[:raise_on_unknown_tag]
3
+ def initialize(options)
4
+ @options = SafeYAML::OPTIONS.merge(options || {})
5
+ @whitelist = @options[:whitelisted_tags] || []
6
+ @initializers = @options[:custom_initializers] || {}
7
+ @raise_on_unknown_tag = @options[:raise_on_unknown_tag]
7
8
  end
8
9
 
9
10
  def resolve_node(node)
10
- if not node
11
- return node
12
- end
13
-
11
+ return node if !node
12
+ return self.native_resolve(node) if tag_is_whitelisted?(self.get_node_tag(node))
13
+
14
14
  case self.get_node_type(node)
15
15
  when :root
16
16
  resolve_root(node)
@@ -28,12 +28,9 @@ module SafeYAML
28
28
  end
29
29
 
30
30
  def resolve_map(node)
31
- tag = get_and_check_node_tag(node)
32
- return self.native_resolve(node) if tag_is_whitelisted?(tag)
33
-
31
+ tag = get_and_check_node_tag(node)
34
32
  hash = @initializers.include?(tag) ? @initializers[tag].call : {}
35
-
36
- map = normalize_map(self.get_node_value(node))
33
+ map = normalize_map(self.get_node_value(node))
37
34
 
38
35
  # Take the "<<" key nodes first, as these are meant to approximate a form of inheritance.
39
36
  inheritors = map.select { |key_node, value_node| resolve_node(key_node) == "<<" }
@@ -59,12 +56,12 @@ module SafeYAML
59
56
  end
60
57
 
61
58
  def resolve_scalar(node)
62
- Transform.to_proper_type(self.get_node_value(node), self.value_is_quoted?(node), get_and_check_node_tag(node))
59
+ Transform.to_proper_type(self.get_node_value(node), self.value_is_quoted?(node), get_and_check_node_tag(node), @options)
63
60
  end
64
61
 
65
62
  def get_and_check_node_tag(node)
66
63
  tag = self.get_node_tag(node)
67
- SafeYAML.tag_safety_check!(tag)
64
+ SafeYAML.tag_safety_check!(tag, @options)
68
65
  tag
69
66
  end
70
67
 
@@ -72,6 +69,10 @@ module SafeYAML
72
69
  @whitelist.include?(tag)
73
70
  end
74
71
 
72
+ def options
73
+ @options
74
+ end
75
+
75
76
  private
76
77
  def normalize_map(map)
77
78
  # Syck creates Hashes from maps.
@@ -8,7 +8,7 @@ module SafeYAML
8
8
  def accept(node)
9
9
  if node.tag
10
10
  return super if @resolver.tag_is_whitelisted?(node.tag)
11
- raise "Unknown YAML tag '#{node.tag}'" if SafeYAML::OPTIONS[:raise_on_unknown_tag]
11
+ raise "Unknown YAML tag '#{node.tag}'" if @resolver.options[:raise_on_unknown_tag]
12
12
  end
13
13
 
14
14
  @resolver.resolve_node(node)
@@ -1,12 +1,34 @@
1
+ # This is, admittedly, pretty insane. Fundamentally the challenge here is this: if we want to allow
2
+ # whitelisting of tags (while still leveraging Syck's internal functionality), then we have to
3
+ # change how Syck::Node#transform works. But since we (SafeYAML) do not control instantiation of
4
+ # Syck::Node objects, we cannot, for example, subclass Syck::Node and override #tranform the "easy"
5
+ # way. So the only choice is to monkeypatch, like this. And the only way to make this work
6
+ # recursively with potentially call-specific options (that my feeble brain can think of) is to set
7
+ # pseudo-global options on the first call and unset them once the recursive stack has fully unwound.
8
+
1
9
  monkeypatch = <<-EORUBY
2
10
  class Node
3
- def safe_transform
4
- if self.type_id
5
- return unsafe_transform if SafeYAML::OPTIONS[:whitelisted_tags].include?(self.type_id)
6
- SafeYAML.tag_safety_check!(self.type_id)
7
- end
11
+ @@safe_transform_depth = 0
12
+ @@safe_transform_whitelist = nil
13
+
14
+ def safe_transform(options={})
15
+ begin
16
+ @@safe_transform_depth += 1
17
+ @@safe_transform_whitelist ||= options[:whitelisted_tags]
8
18
 
9
- SafeYAML::SyckResolver.new.resolve_node(self)
19
+ if self.type_id
20
+ SafeYAML.tag_safety_check!(self.type_id, options)
21
+ return unsafe_transform if @@safe_transform_whitelist.include?(self.type_id)
22
+ end
23
+
24
+ SafeYAML::SyckResolver.new.resolve_node(self)
25
+
26
+ ensure
27
+ @@safe_transform_depth -= 1
28
+ if @@safe_transform_depth == 0
29
+ @@safe_transform_whitelist = nil
30
+ end
31
+ end
10
32
  end
11
33
 
12
34
  alias_method :unsafe_transform, :transform
@@ -1,6 +1,9 @@
1
1
  module SafeYAML
2
2
  class SyckResolver < Resolver
3
- QUOTE_STYLES = [:quote1, :quote2].freeze
3
+ QUOTE_STYLES = [
4
+ :quote1,
5
+ :quote2
6
+ ].freeze
4
7
 
5
8
  NODE_TYPES = {
6
9
  Hash => :map,
@@ -8,12 +11,12 @@ module SafeYAML
8
11
  String => :scalar
9
12
  }.freeze
10
13
 
11
- def initialize
12
- super()
14
+ def initialize(options={})
15
+ super
13
16
  end
14
17
 
15
18
  def native_resolve(node)
16
- node.transform
19
+ node.transform(self.options)
17
20
  end
18
21
 
19
22
  def get_node_type(node)
@@ -11,12 +11,15 @@ module SafeYAML
11
11
  Transform::ToDate.new
12
12
  ]
13
13
 
14
- def self.to_guessed_type(value, quoted=false)
14
+ def self.to_guessed_type(value, quoted=false, options=nil)
15
15
  return value if quoted
16
16
 
17
17
  if value.is_a?(String)
18
18
  TRANSFORMERS.each do |transformer|
19
- success, transformed_value = transformer.transform?(value)
19
+ success, transformed_value = transformer.method(:transform?).arity == 1 ?
20
+ transformer.transform?(value) :
21
+ transformer.transform?(value, options)
22
+
20
23
  return transformed_value if success
21
24
  end
22
25
  end
@@ -24,14 +27,14 @@ module SafeYAML
24
27
  value
25
28
  end
26
29
 
27
- def self.to_proper_type(value, quoted=false, tag=nil)
30
+ def self.to_proper_type(value, quoted=false, tag=nil, options=nil)
28
31
  case tag
29
32
  when "tag:yaml.org,2002:binary", "x-private:binary", "!binary"
30
33
  decoded = Base64.decode64(value)
31
34
  decoded = decoded.force_encoding(value.encoding) if decoded.respond_to?(:force_encoding)
32
35
  decoded
33
36
  else
34
- self.to_guessed_type(value, quoted)
37
+ self.to_guessed_type(value, quoted, options)
35
38
  end
36
39
  end
37
40
  end
@@ -3,8 +3,9 @@ module SafeYAML
3
3
  class ToSymbol
4
4
  MATCHER = /\A:"?(\w+)"?\Z/.freeze
5
5
 
6
- def transform?(value)
7
- return false unless SafeYAML::OPTIONS[:deserialize_symbols] && MATCHER.match(value)
6
+ def transform?(value, options=nil)
7
+ options ||= SafeYAML::OPTIONS
8
+ return false unless options[:deserialize_symbols] && MATCHER.match(value)
8
9
  return true, $1.to_sym
9
10
  end
10
11
  end
@@ -1,3 +1,3 @@
1
1
  module SafeYAML
2
- VERSION = "0.8.6"
2
+ VERSION = "0.9.0"
3
3
  end
@@ -169,26 +169,20 @@ module ResolverSpecs
169
169
  end
170
170
  end
171
171
 
172
- # This does in fact appear to be a Ruby version thing as opposed to a YAML engine thing.
173
172
  context "for Ruby version #{RUBY_VERSION}" do
174
- if RUBY_VERSION >= "1.8.7"
175
- it "translates valid time values" do
176
- parse "time: 2013-01-29 05:58:00 -0800"
177
- result.should == { "time" => Time.utc(2013, 1, 29, 13, 58, 0) }
178
- end
179
-
180
- it "applies the same transformation to elements in sequences" do
181
- parse "- 2013-01-29 05:58:00 -0800"
182
- result.should == [Time.utc(2013, 1, 29, 13, 58, 0)]
183
- end
184
-
185
- # On Ruby 2.0.0-rc1, even YAML.load overflows the stack on this input.
186
- if RUBY_VERSION != "2.0.0"
187
- it "applies the same transformation to keys" do
188
- parse "2013-01-29 05:58:00 -0800: time"
189
- result.should == { Time.utc(2013, 1, 29, 13, 58, 0) => "time" }
190
- end
191
- end
173
+ it "translates valid time values" do
174
+ parse "time: 2013-01-29 05:58:00 -0800"
175
+ result.should == { "time" => Time.utc(2013, 1, 29, 13, 58, 0) }
176
+ end
177
+
178
+ it "applies the same transformation to elements in sequences" do
179
+ parse "- 2013-01-29 05:58:00 -0800"
180
+ result.should == [Time.utc(2013, 1, 29, 13, 58, 0)]
181
+ end
182
+
183
+ it "applies the same transformation to keys" do
184
+ parse "2013-01-29 05:58:00 -0800: time"
185
+ result.should == { Time.utc(2013, 1, 29, 13, 58, 0) => "time" }
192
186
  end
193
187
  end
194
188
 
@@ -11,10 +11,23 @@ describe YAML do
11
11
  $VERBOSE = true
12
12
  end
13
13
 
14
+ def safe_load_round_trip(object, options={})
15
+ yaml = object.to_yaml
16
+ if SafeYAML::YAML_ENGINE == "psych"
17
+ YAML.safe_load(yaml, nil, options)
18
+ else
19
+ YAML.safe_load(yaml, options)
20
+ end
21
+ end
22
+
14
23
  before :each do
15
24
  SafeYAML.restore_defaults!
16
25
  end
17
26
 
27
+ after :each do
28
+ SafeYAML.restore_defaults!
29
+ end
30
+
18
31
  describe "unsafe_load" do
19
32
  if SafeYAML::YAML_ENGINE == "psych" && RUBY_VERSION >= "1.9.3"
20
33
  it "allows exploits through objects defined in YAML w/ !ruby/hash via custom :[]= methods" do
@@ -35,11 +48,7 @@ describe YAML do
35
48
 
36
49
  context "with special whitelisted tags defined" do
37
50
  before :each do
38
- if SafeYAML::YAML_ENGINE == "psych"
39
- SafeYAML::OPTIONS[:whitelisted_tags] = ["!ruby/object:OpenStruct"]
40
- else
41
- SafeYAML::OPTIONS[:whitelisted_tags] = ["tag:ruby.yaml.org,2002:object:OpenStruct"]
42
- end
51
+ SafeYAML::whitelist!(OpenStruct)
43
52
  end
44
53
 
45
54
  it "effectively ignores the whitelist (since everything is whitelisted)" do
@@ -69,18 +78,27 @@ describe YAML do
69
78
 
70
79
  context "for YAML engine #{SafeYAML::YAML_ENGINE}" do
71
80
  if SafeYAML::YAML_ENGINE == "psych"
72
- let(:arguments) {
73
- if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
74
- ["foo: bar", nil]
75
- else
76
- ["foo: bar"]
81
+ let(:options) { nil }
82
+ let(:arguments) { ["foo: bar", nil, options] }
83
+
84
+ context "when no tags are whitelisted" do
85
+ it "constructs a SafeYAML::PsychHandler to resolve nodes as they're parsed, for optimal performance" do
86
+ Psych::Parser.should_receive(:new).with an_instance_of(SafeYAML::PsychHandler)
87
+ # This won't work now; we just want to ensure Psych::Parser#parse was in fact called.
88
+ YAML.safe_load(*arguments) rescue nil
77
89
  end
78
- }
90
+ end
91
+
92
+ context "when whitelisted tags are specified" do
93
+ let(:options) {
94
+ { :whitelisted_tags => ["foo"] }
95
+ }
79
96
 
80
- it "uses Psych internally to parse YAML" do
81
- Psych.should_receive(:parse).with(*arguments)
82
- # This won't work now; we just want to ensure Psych::Parser#parse was in fact called.
83
- YAML.safe_load(*arguments) rescue nil
97
+ it "instead uses Psych to construct a full tree before examining the nodes" do
98
+ Psych.should_receive(:parse)
99
+ # This won't work now; we just want to ensure Psych::Parser#parse was in fact called.
100
+ YAML.safe_load(*arguments) rescue nil
101
+ end
84
102
  end
85
103
  end
86
104
 
@@ -281,11 +299,7 @@ describe YAML do
281
299
 
282
300
  context "with special whitelisted tags defined" do
283
301
  before :each do
284
- if SafeYAML::YAML_ENGINE == "psych"
285
- SafeYAML::OPTIONS[:whitelisted_tags] = ["!ruby/object:OpenStruct"]
286
- else
287
- SafeYAML::OPTIONS[:whitelisted_tags] = ["tag:ruby.yaml.org,2002:object:OpenStruct"]
288
- end
302
+ SafeYAML::whitelist!(OpenStruct)
289
303
 
290
304
  # Necessary for deserializing OpenStructs properly.
291
305
  SafeYAML::OPTIONS[:deserialize_symbols] = true
@@ -372,6 +386,56 @@ describe YAML do
372
386
  end
373
387
  end
374
388
  end
389
+
390
+ context "when options are passed direclty to #load which differ from the defaults" do
391
+ let(:default_options) { {} }
392
+
393
+ before :each do
394
+ SafeYAML::OPTIONS.merge!(default_options)
395
+ end
396
+
397
+ context "(for example, when symbol deserialization is enabled by default)" do
398
+ let(:default_options) { { :deserialize_symbols => true } }
399
+
400
+ it "goes with the default option when it is not overridden" do
401
+ silence_warnings do
402
+ YAML.load(":foo: bar").should == { :foo => "bar" }
403
+ end
404
+ end
405
+
406
+ it "allows the default option to be overridden on a per-call basis" do
407
+ silence_warnings do
408
+ YAML.load(":foo: bar", :deserialize_symbols => false).should == { ":foo" => "bar" }
409
+ YAML.load(":foo: bar", :deserialize_symbols => true).should == { :foo => "bar" }
410
+ end
411
+ end
412
+ end
413
+
414
+ context "(or, for example, when certain tags are whitelisted)" do
415
+ let(:default_options) {
416
+ {
417
+ :deserialize_symbols => true,
418
+ :whitelisted_tags => SafeYAML::YAML_ENGINE == "psych" ?
419
+ ["!ruby/object:OpenStruct"] :
420
+ ["tag:ruby.yaml.org,2002:object:OpenStruct"]
421
+ }
422
+ }
423
+
424
+ it "goes with the default option when it is not overridden" do
425
+ result = safe_load_round_trip(OpenStruct.new(:foo => "bar"))
426
+ result.should be_a(OpenStruct)
427
+ result.foo.should == "bar"
428
+ end
429
+
430
+ it "allows the default option to be overridden on a per-call basis" do
431
+ result = safe_load_round_trip(OpenStruct.new(:foo => "bar"), :whitelisted_tags => [])
432
+ result.should == { "table" => { :foo => "bar" } }
433
+
434
+ result = safe_load_round_trip(OpenStruct.new(:foo => "bar"), :deserialize_symbols => false, :whitelisted_tags => [])
435
+ result.should == { "table" => { ":foo" => "bar" } }
436
+ end
437
+ end
438
+ end
375
439
  end
376
440
 
377
441
  describe "unsafe_load_file" do
@@ -408,11 +472,13 @@ describe YAML do
408
472
  end
409
473
 
410
474
  describe "load" do
475
+ let(:options) { {} }
476
+
411
477
  let (:arguments) {
412
478
  if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
413
- ["foo: bar", nil]
479
+ ["foo: bar", nil, options]
414
480
  else
415
- ["foo: bar"]
481
+ ["foo: bar", options]
416
482
  end
417
483
  }
418
484
 
@@ -430,7 +496,31 @@ describe YAML do
430
496
  end
431
497
  end
432
498
 
433
- it "issues a warning if the :safe option is omitted" do
499
+ context "when the :safe options is specified" do
500
+ let(:safe_mode) { true }
501
+ let(:options) { { :safe => safe_mode } }
502
+
503
+ it "doesn't issue a warning" do
504
+ Kernel.should_not_receive(:warn)
505
+ YAML.load(*arguments)
506
+ end
507
+
508
+ it "calls #safe_load if the :safe option is set to true" do
509
+ YAML.should_receive(:safe_load)
510
+ YAML.load(*arguments)
511
+ end
512
+
513
+ context "when the :safe option is set to false" do
514
+ let(:safe_mode) { false }
515
+
516
+ it "calls #unsafe_load if the :safe option is set to false" do
517
+ YAML.should_receive(:unsafe_load)
518
+ YAML.load(*arguments)
519
+ end
520
+ end
521
+ end
522
+
523
+ it "issues a warning when the :safe option is omitted" do
434
524
  silence_warnings do
435
525
  Kernel.should_receive(:warn)
436
526
  YAML.load(*arguments)
@@ -444,43 +534,28 @@ describe YAML do
444
534
  end
445
535
  end
446
536
 
447
- it "doesn't issue a warning as long as the :safe option is specified" do
448
- Kernel.should_not_receive(:warn)
449
- YAML.load(*(arguments + [{:safe => true}]))
450
- end
451
-
452
537
  it "defaults to safe mode if the :safe option is omitted" do
453
538
  silence_warnings do
454
- YAML.should_receive(:safe_load).with(*arguments)
539
+ YAML.should_receive(:safe_load)
455
540
  YAML.load(*arguments)
456
541
  end
457
542
  end
458
543
 
459
- it "calls #safe_load if the :safe option is set to true" do
460
- YAML.should_receive(:safe_load).with(*arguments)
461
- YAML.load(*(arguments + [{:safe => true}]))
462
- end
463
-
464
- it "calls #unsafe_load if the :safe option is set to false" do
465
- YAML.should_receive(:unsafe_load).with(*arguments)
466
- YAML.load(*(arguments + [{:safe => false}]))
467
- end
468
-
469
- context "with arbitrary object deserialization enabled by default" do
544
+ context "with the default mode set to :unsafe" do
470
545
  before :each do
471
546
  SafeYAML::OPTIONS[:default_mode] = :unsafe
472
547
  end
473
548
 
474
549
  it "defaults to unsafe mode if the :safe option is omitted" do
475
550
  silence_warnings do
476
- YAML.should_receive(:unsafe_load).with(*arguments)
551
+ YAML.should_receive(:unsafe_load)
477
552
  YAML.load(*arguments)
478
553
  end
479
554
  end
480
555
 
481
556
  it "calls #safe_load if the :safe option is set to true" do
482
- YAML.should_receive(:safe_load).with(*arguments)
483
- YAML.load(*(arguments + [{:safe => true}]))
557
+ YAML.should_receive(:safe_load)
558
+ YAML.load(*(arguments + [{ :safe => true }]))
484
559
  end
485
560
  end
486
561
  end
@@ -502,18 +577,18 @@ describe YAML do
502
577
 
503
578
  it "defaults to safe mode if the :safe option is omitted" do
504
579
  silence_warnings do
505
- YAML.should_receive(:safe_load_file).with(filename)
580
+ YAML.should_receive(:safe_load_file)
506
581
  YAML.load_file(filename)
507
582
  end
508
583
  end
509
584
 
510
585
  it "calls #safe_load_file if the :safe option is set to true" do
511
- YAML.should_receive(:safe_load_file).with(filename)
586
+ YAML.should_receive(:safe_load_file)
512
587
  YAML.load_file(filename, :safe => true)
513
588
  end
514
589
 
515
590
  it "calls #unsafe_load_file if the :safe option is set to false" do
516
- YAML.should_receive(:unsafe_load_file).with(filename)
591
+ YAML.should_receive(:unsafe_load_file)
517
592
  YAML.load_file(filename, :safe => false)
518
593
  end
519
594
 
@@ -524,15 +599,80 @@ describe YAML do
524
599
 
525
600
  it "defaults to unsafe mode if the :safe option is omitted" do
526
601
  silence_warnings do
527
- YAML.should_receive(:unsafe_load_file).with(filename)
602
+ YAML.should_receive(:unsafe_load_file)
528
603
  YAML.load_file(filename)
529
604
  end
530
605
  end
531
606
 
532
607
  it "calls #safe_load if the :safe option is set to true" do
533
- YAML.should_receive(:safe_load_file).with(filename)
608
+ YAML.should_receive(:safe_load_file)
534
609
  YAML.load_file(filename, :safe => true)
535
610
  end
536
611
  end
537
612
  end
613
+
614
+ describe "whitelist!" do
615
+ context "not a class" do
616
+ it "should raise" do
617
+ expect { SafeYAML::whitelist! :foo }.to raise_error(/not a Class/)
618
+ SafeYAML::OPTIONS[:whitelisted_tags].should be_empty
619
+ end
620
+ end
621
+
622
+ context "anonymous class" do
623
+ it "should raise" do
624
+ expect { SafeYAML::whitelist! Class.new }.to raise_error(/cannot be anonymous/)
625
+ SafeYAML::OPTIONS[:whitelisted_tags].should be_empty
626
+ end
627
+ end
628
+
629
+ context "with a Class as its argument" do
630
+ it "should configure correctly" do
631
+ expect { SafeYAML::whitelist! OpenStruct }.to_not raise_error
632
+ SafeYAML::OPTIONS[:whitelisted_tags].grep(/OpenStruct\Z/).should_not be_empty
633
+ end
634
+
635
+ it "successfully deserializes the specified class" do
636
+ SafeYAML.whitelist!(OpenStruct)
637
+
638
+ # necessary for properly assigning OpenStruct attributes
639
+ SafeYAML::OPTIONS[:deserialize_symbols] = true
640
+
641
+ result = safe_load_round_trip(OpenStruct.new(:foo => "bar"))
642
+ result.should be_a(OpenStruct)
643
+ result.foo.should == "bar"
644
+ end
645
+
646
+ it "works for ranges" do
647
+ SafeYAML.whitelist!(Range)
648
+ safe_load_round_trip(1..10).should == (1..10)
649
+ end
650
+
651
+ it "works for regular expressions" do
652
+ SafeYAML.whitelist!(Regexp)
653
+ safe_load_round_trip(/foo/).should == /foo/
654
+ end
655
+
656
+ it "works for multiple classes" do
657
+ SafeYAML.whitelist!(Range, Regexp)
658
+ safe_load_round_trip([(1..10), /bar/]).should == [(1..10), /bar/]
659
+ end
660
+
661
+ it "works for arbitrary Exception subclasses" do
662
+ class CustomException < Exception
663
+ attr_reader :custom_message
664
+
665
+ def initialize(custom_message)
666
+ @custom_message = custom_message
667
+ end
668
+ end
669
+
670
+ SafeYAML.whitelist!(CustomException)
671
+
672
+ ex = safe_load_round_trip(CustomException.new("blah"))
673
+ ex.should be_a(CustomException)
674
+ ex.custom_message.should == "blah"
675
+ end
676
+ end
677
+ end
538
678
  end
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: safe_yaml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.6
4
+ version: 0.9.0
5
+ prerelease:
5
6
  platform: ruby
6
7
  authors:
7
8
  - Dan Tao
8
9
  autorequire:
9
10
  bindir: bin
10
11
  cert_chain: []
11
- date: 2013-03-17 00:00:00.000000000 Z
12
+ date: 2013-03-23 00:00:00.000000000 Z
12
13
  dependencies: []
13
14
  description: Parse YAML safely, without that pesky arbitrary object deserialization
14
15
  vulnerability
@@ -24,9 +25,11 @@ files:
24
25
  - README.md
25
26
  - Rakefile
26
27
  - lib/safe_yaml.rb
28
+ - lib/safe_yaml/deep.rb
27
29
  - lib/safe_yaml/parse/date.rb
28
30
  - lib/safe_yaml/parse/hexadecimal.rb
29
31
  - lib/safe_yaml/parse/sexagesimal.rb
32
+ - lib/safe_yaml/psych_handler.rb
30
33
  - lib/safe_yaml/psych_resolver.rb
31
34
  - lib/safe_yaml/resolver.rb
32
35
  - lib/safe_yaml/safe_to_ruby_visitor.rb
@@ -58,26 +61,27 @@ files:
58
61
  homepage: http://dtao.github.com/safe_yaml/
59
62
  licenses:
60
63
  - MIT
61
- metadata: {}
62
64
  post_install_message:
63
65
  rdoc_options: []
64
66
  require_paths:
65
67
  - lib
66
68
  required_ruby_version: !ruby/object:Gem::Requirement
69
+ none: false
67
70
  requirements:
68
- - - '>='
71
+ - - ! '>='
69
72
  - !ruby/object:Gem::Version
70
73
  version: 1.8.7
71
74
  required_rubygems_version: !ruby/object:Gem::Requirement
75
+ none: false
72
76
  requirements:
73
- - - '>='
77
+ - - ! '>='
74
78
  - !ruby/object:Gem::Version
75
79
  version: '0'
76
80
  requirements: []
77
81
  rubyforge_project:
78
- rubygems_version: 2.0.0
82
+ rubygems_version: 1.8.25
79
83
  signing_key:
80
- specification_version: 4
84
+ specification_version: 3
81
85
  summary: SameYAML provides an alternative implementation of YAML.load suitable for
82
86
  accepting user input in Ruby applications.
83
87
  test_files:
checksums.yaml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- SHA1:
3
- metadata.gz: cacc423c7a4bb7bafd942e34978dc51e317bb483
4
- data.tar.gz: 540a9ddba02abd5993e1f0901907983b4aea7088
5
- SHA512:
6
- metadata.gz: 348b33b5126f29f523580988749fa8c260a560868d07f2bb1dccd5ea723aaf761aa26a2c19be1a8c102092edf4402b780ffaa49090b6c28f721a7e6fc27e3ab4
7
- data.tar.gz: a66a0b50feec1e5db74382340ce36ff1ef909fa27f74f44d702a9e2e131e8285a7700978bc4b8ed6b2e1ab63e960c93fcdce2385bf635c289a9bc14a243b430d