safe_yaml 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -1,8 +1,9 @@
1
- source :rubygems
1
+ source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
4
 
5
5
  group :development do
6
+ gem "hashie"
6
7
  gem "heredoc_unindent"
7
8
  gem "rake"
8
9
  gem "rspec"
data/README.md CHANGED
@@ -3,7 +3,7 @@ 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 one recently discovered in Rails](http://www.reddit.com/r/netsec/comments/167c11/serious_vulnerability_in_ruby_on_rails_allowing/) (or [this one](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 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)).
7
7
 
8
8
  Installation
9
9
  ------------
@@ -47,11 +47,12 @@ Now, if you were to use `YAML.load` on user input anywhere in your application w
47
47
 
48
48
  Observe:
49
49
 
50
- > yaml = <<-EOYAML
51
- > --- !ruby/hash:ExploitableClassBuilder
52
- > "foo; end; puts %(I'm in yr system!); def bar": "baz"
53
- > EOYAML
54
- => "--- !ruby/hash:ExploitableClassBuilder\n\"foo; end; puts %(I'm in yr system!); def bar\": \"baz\"\n"
50
+ ```ruby
51
+ yaml = <<-EOYAML
52
+ --- !ruby/hash:ExploitableClassBuilder
53
+ "foo; end; puts %(I'm in yr system!); def bar": "baz"
54
+ EOYAML
55
+ ```
55
56
 
56
57
  > YAML.load(yaml)
57
58
  I'm in yr system!
@@ -61,7 +62,7 @@ With SafeYAML, that attacker would be thwarted:
61
62
 
62
63
  > require "safe_yaml"
63
64
  => true
64
- > YAML.load(yaml)
65
+ > YAML.safe_load(yaml)
65
66
  => {"foo; end; puts %(I'm in yr system!); def bar"=>"baz"}
66
67
 
67
68
  Usage
@@ -73,20 +74,22 @@ Usage
73
74
 
74
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:
75
76
 
76
- # Ruby >= 1.9.3
77
- YAML.load(yaml, filename, :safe => true) # calls safe_load
78
- YAML.load(yaml, filename, :safe => false) # calls unsafe_load
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
79
81
 
80
- # Ruby < 1.9.3
81
- YAML.load(yaml, :safe => true) # calls safe_load
82
- YAML.load(yaml, :safe => false) # calls unsafe_load
82
+ # Ruby < 1.9.3
83
+ YAML.load(yaml, :safe => true) # calls safe_load
84
+ YAML.load(yaml, :safe => false) # calls unsafe_load
85
+ ```
83
86
 
84
- The default behavior can be switched to unsafe loading by calling `YAML.enable_arbitrary_object_deserialization!`. 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).
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).
85
88
 
86
- This gem will also warn you whenever you use `YAML.load` without specifying the `:safe` option. If you do not want to see these messages in your logs, you can say `SafeYAML::OPTIONS[:suppress_warnings] = true` in an initializer.
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.
87
90
 
88
- Notes
89
- -----
91
+ Supported Types
92
+ ---------------
90
93
 
91
94
  The way that SafeYAML works is by restricting the kinds of objects that can be deserialized via `YAML.load`. More specifically, only the following types of objects can be deserialized by default:
92
95
 
@@ -99,15 +102,46 @@ The way that SafeYAML works is by restricting the kinds of objects that can be d
99
102
  - Booleans
100
103
  - Nils
101
104
 
102
- Additionally, deserialization of symbols can be enabled by calling `YAML.enable_symbol_parsing!` (for example, in an initializer).
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).
106
+
107
+ Whitelisting Trusted Types
108
+ --------------------------
109
+
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:
111
+
112
+ ```ruby
113
+ # Using Syck (unfortunately, Syck and Psych use different tagging schemes)
114
+ SafeYAML::OPTIONS[:whitelisted_tags] = ["tag:ruby.yaml.org,2002:object:OpenStruct"]
115
+
116
+ # Using Psych
117
+ SafeYAML::OPTIONS[:whitelisted_tags] = ["!ruby/object:OpenStruct"]
118
+ ```
119
+
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:
123
+
124
+ ```ruby
125
+ yaml = <<-EOYAML
126
+ --- !ruby/object:OpenStruct
127
+ table:
128
+ :backdoor: !ruby/hash:ExploitableClassBuilder
129
+ "foo; end; puts %(I'm in yr system!); def bar": "baz"
130
+ EOYAML
131
+ ```
132
+
133
+ > YAML.safe_load(yaml)
134
+ => #<OpenStruct :backdoor={"foo; end; puts %(I'm in yr system!); def bar"=>"baz"}>
135
+
136
+ Pretty sweet, right?
103
137
 
104
138
  Known Issues
105
139
  ------------
106
140
 
107
- Also note 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:
141
+ 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:
108
142
 
109
- - [**Guard**](https://github.com/guard/guard): Uses YAML as a serialization format for notifications. The data serialized uses symbolic keys, so calling `YAML.enable_symbol_parsing!` is necessary to allow Guard to work.
110
- - [**sidekiq**](https://github.com/mperham/sidekiq): Uses a YAML configiuration file with symbolic keys, so calling `YAML.enable_symbol_parsing!` should allow it to work.
143
+ - [**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.
144
+ - [**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.
111
145
 
112
146
  The above list will grow over time, as more issues are discovered.
113
147
 
data/lib/safe_yaml.rb CHANGED
@@ -7,17 +7,25 @@ require "safe_yaml/transform/to_nil"
7
7
  require "safe_yaml/transform/to_symbol"
8
8
  require "safe_yaml/transform/to_time"
9
9
  require "safe_yaml/transform"
10
- require "safe_yaml/version"
10
+ require "safe_yaml/resolver"
11
11
 
12
12
  module SafeYAML
13
13
  MULTI_ARGUMENT_YAML_LOAD = YAML.method(:load).arity != 1
14
14
  YAML_ENGINE = defined?(YAML::ENGINE) ? YAML::ENGINE.yamler : "syck"
15
15
 
16
- OPTIONS = {
17
- :enable_symbol_parsing => false,
18
- :enable_arbitrary_object_deserialization => false,
19
- :suppress_warnings => false
20
- }
16
+ DEFAULT_OPTIONS = {
17
+ :custom_initializers => {},
18
+ :default_mode => nil,
19
+ :deserialize_symbols => false,
20
+ :whitelisted_tags => []
21
+ }.freeze
22
+
23
+ OPTIONS = DEFAULT_OPTIONS.dup
24
+
25
+ module_function
26
+ def reset_defaults!
27
+ OPTIONS.merge!(DEFAULT_OPTIONS)
28
+ end
21
29
  end
22
30
 
23
31
  module YAML
@@ -26,25 +34,25 @@ module YAML
26
34
  safe_mode = safe_mode_from_options("load", options)
27
35
  arguments = [yaml]
28
36
  arguments << filename_and_options.first if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
29
- safe_mode ? safe_load(*arguments) : unsafe_load(*arguments)
37
+ safe_mode == :safe ? safe_load(*arguments) : unsafe_load(*arguments)
30
38
  end
31
39
 
32
40
  def self.load_file_with_options(file, options={})
33
41
  safe_mode = safe_mode_from_options("load_file", options)
34
- safe_mode ? safe_load_file(file) : unsafe_load_file(file)
42
+ safe_mode == :safe ? safe_load_file(file) : unsafe_load_file(file)
35
43
  end
36
44
 
37
45
  if SafeYAML::YAML_ENGINE == "psych"
38
- require "safe_yaml/psych_handler"
46
+ require "safe_yaml/psych_visitor"
47
+ require "safe_yaml/psych_resolver"
39
48
  def self.safe_load(yaml, filename=nil)
40
- safe_handler = SafeYAML::PsychHandler.new
41
- parser = Psych::Parser.new(safe_handler)
42
- if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
43
- parser.parse(yaml, filename)
49
+ safe_resolver = SafeYAML::PsychResolver.new
50
+ tree = if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
51
+ Psych.parse(yaml, filename)
44
52
  else
45
- parser.parse(yaml)
53
+ Psych.parse(yaml)
46
54
  end
47
- return safe_handler.result
55
+ return safe_resolver.resolve_node(tree)
48
56
  end
49
57
 
50
58
  def self.safe_load_file(filename)
@@ -63,10 +71,12 @@ module YAML
63
71
 
64
72
  else
65
73
  require "safe_yaml/syck_resolver"
74
+ require "safe_yaml/syck_node_monkeypatch"
75
+
66
76
  def self.safe_load(yaml)
67
- safe_resolver = SafeYAML::SyckResolver.new
77
+ resolver = SafeYAML::SyckResolver.new
68
78
  tree = YAML.parse(yaml)
69
- return safe_resolver.resolve_node(tree)
79
+ return resolver.resolve_node(tree)
70
80
  end
71
81
 
72
82
  def self.safe_load_file(filename)
@@ -85,40 +95,49 @@ module YAML
85
95
  alias_method :load_file, :load_file_with_options
86
96
 
87
97
  def enable_symbol_parsing?
88
- SafeYAML::OPTIONS[:enable_symbol_parsing]
98
+ warn_of_deprecated_method("set the SafeYAML::OPTIONS[:deserialize_symbols] option instead")
99
+ SafeYAML::OPTIONS[:deserialize_symbols]
89
100
  end
90
101
 
91
102
  def enable_symbol_parsing!
92
- SafeYAML::OPTIONS[:enable_symbol_parsing] = true
103
+ warn_of_deprecated_method("set the SafeYAML::OPTIONS[:deserialize_symbols] option instead")
104
+ SafeYAML::OPTIONS[:deserialize_symbols] = true
93
105
  end
94
106
 
95
107
  def disable_symbol_parsing!
96
- SafeYAML::OPTIONS[:enable_symbol_parsing] = false
108
+ warn_of_deprecated_method("set the SafeYAML::OPTIONS[:deserialize_symbols] option instead")
109
+ SafeYAML::OPTIONS[:deserialize_symbols] = false
97
110
  end
98
111
 
99
112
  def enable_arbitrary_object_deserialization?
100
- SafeYAML::OPTIONS[:enable_arbitrary_object_deserialization]
113
+ warn_of_deprecated_method("set the SafeYAML::OPTIONS[:default_mode] to either :safe or :unsafe")
114
+ SafeYAML::OPTIONS[:default_mode] == :unsafe
101
115
  end
102
116
 
103
117
  def enable_arbitrary_object_deserialization!
104
- SafeYAML::OPTIONS[:enable_arbitrary_object_deserialization] = true
118
+ warn_of_deprecated_method("set the SafeYAML::OPTIONS[:default_mode] to either :safe or :unsafe")
119
+ SafeYAML::OPTIONS[:default_mode] = :unsafe
105
120
  end
106
121
 
107
122
  def disable_arbitrary_object_deserialization!
108
- SafeYAML::OPTIONS[:enable_arbitrary_object_deserialization] = false
123
+ warn_of_deprecated_method("set the SafeYAML::OPTIONS[:default_mode] to either :safe or :unsafe")
124
+ SafeYAML::OPTIONS[:default_mode] = :safe
109
125
  end
110
126
 
111
127
  private
112
128
  def safe_mode_from_options(method, options={})
113
- safe_mode = options[:safe]
114
-
115
- if safe_mode.nil?
116
- mode = SafeYAML::OPTIONS[:enable_arbitrary_object_deserialization] ? "unsafe" : "safe"
117
- Kernel.warn "Called '#{method}' without the :safe option -- defaulting to #{mode} mode." unless SafeYAML::OPTIONS[:suppress_warnings]
118
- safe_mode = !SafeYAML::OPTIONS[:enable_arbitrary_object_deserialization]
129
+ if options[:safe].nil?
130
+ safe_mode = SafeYAML::OPTIONS[:default_mode] || :safe
131
+ Kernel.warn "Called '#{method}' without the :safe option -- defaulting to #{safe_mode} mode." if SafeYAML::OPTIONS[:default_mode].nil?
132
+ return safe_mode
119
133
  end
120
134
 
121
- safe_mode
135
+ options[:safe] ? :safe : :unsafe
136
+ end
137
+
138
+ def warn_of_deprecated_method(message)
139
+ method = caller.first[/`([^']*)'$/, 1]
140
+ Kernel.warn("The method 'YAML.#{method}' is deprecated and will be removed in the next release of SafeYAML -- #{message}.")
122
141
  end
123
142
  end
124
143
  end
@@ -0,0 +1,52 @@
1
+ module SafeYAML
2
+ class PsychResolver < Resolver
3
+ NODE_TYPES = {
4
+ Psych::Nodes::Document => :root,
5
+ Psych::Nodes::Mapping => :map,
6
+ Psych::Nodes::Sequence => :seq,
7
+ Psych::Nodes::Scalar => :scalar,
8
+ Psych::Nodes::Alias => :alias
9
+ }.freeze
10
+
11
+ def initialize
12
+ super()
13
+ @aliased_nodes = {}
14
+ end
15
+
16
+ def resolve_root(root)
17
+ resolve_seq(root).first
18
+ end
19
+
20
+ def resolve_alias(node)
21
+ resolve_node(@aliased_nodes[node.anchor])
22
+ end
23
+
24
+ def native_resolve(node)
25
+ @visitor ||= SafeYAML::PsychVisitor.new(self)
26
+ @visitor.accept(node)
27
+ end
28
+
29
+ def get_node_type(node)
30
+ NODE_TYPES[node.class]
31
+ end
32
+
33
+ def get_node_tag(node)
34
+ node.tag
35
+ end
36
+
37
+ def get_node_value(node)
38
+ @aliased_nodes[node.anchor] = node if node.respond_to?(:anchor) && node.anchor
39
+
40
+ case get_node_type(node)
41
+ when :root, :map, :seq
42
+ node.children
43
+ when :scalar
44
+ node.value
45
+ end
46
+ end
47
+
48
+ def value_is_quoted?(node)
49
+ node.quoted
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,13 @@
1
+ module SafeYAML
2
+ class PsychVisitor < Psych::Visitors::ToRuby
3
+ def initialize(resolver)
4
+ super()
5
+ @resolver = resolver
6
+ end
7
+
8
+ def accept(node)
9
+ return super if @resolver.tag_is_whitelisted?(@resolver.get_node_tag(node))
10
+ @resolver.resolve_node(node)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,82 @@
1
+ module SafeYAML
2
+ class Resolver
3
+ def initialize
4
+ @whitelist = SafeYAML::OPTIONS[:whitelisted_tags] || []
5
+ @initializers = SafeYAML::OPTIONS[:custom_initializers] || {}
6
+ end
7
+
8
+ def resolve_node(node)
9
+ case self.get_node_type(node)
10
+ when :root
11
+ resolve_root(node)
12
+ when :map
13
+ resolve_map(node)
14
+ when :seq
15
+ resolve_seq(node)
16
+ when :scalar
17
+ resolve_scalar(node)
18
+ when :alias
19
+ resolve_alias(node)
20
+ else
21
+ raise "Don't know how to resolve this node: #{node.inspect}"
22
+ end
23
+ end
24
+
25
+ def resolve_map(node)
26
+ tag = self.get_node_tag(node)
27
+ return self.native_resolve(node) if tag_is_whitelisted?(tag)
28
+
29
+ hash = @initializers.include?(tag) ? @initializers[tag].call : {}
30
+
31
+ map = normalize_map(self.get_node_value(node))
32
+
33
+ # Take the "<<" key nodes first, as these are meant to approximate a form of inheritance.
34
+ inheritors = map.select { |key_node, value_node| resolve_node(key_node) == "<<" }
35
+ inheritors.each do |key_node, value_node|
36
+ merge_into_hash(hash, resolve_node(value_node))
37
+ end
38
+
39
+ # All that's left should be normal (non-"<<") nodes.
40
+ (map - inheritors).each do |key_node, value_node|
41
+ hash[resolve_node(key_node)] = resolve_node(value_node)
42
+ end
43
+
44
+ return hash
45
+ end
46
+
47
+ def resolve_seq(node)
48
+ seq = self.get_node_value(node)
49
+
50
+ tag = get_node_tag(node)
51
+ arr = @initializers.include?(tag) ? @initializers[tag].call : []
52
+
53
+ seq.inject(arr) { |array, node| array << resolve_node(node) }
54
+ end
55
+
56
+ def resolve_scalar(node)
57
+ Transform.to_proper_type(self.get_node_value(node), self.value_is_quoted?(node), self.get_node_tag(node))
58
+ end
59
+
60
+ def tag_is_whitelisted?(tag)
61
+ @whitelist.include?(tag)
62
+ end
63
+
64
+ private
65
+ def normalize_map(map)
66
+ # Syck creates Hashes from maps.
67
+ if map.is_a?(Hash)
68
+ map.inject([]) { |arr, key_and_value| arr << key_and_value }
69
+
70
+ # Psych is really weird; it flattens out a Hash completely into: [key, value, key, value, ...]
71
+ else
72
+ map.each_slice(2).to_a
73
+ end
74
+ end
75
+
76
+ def merge_into_hash(hash, array)
77
+ array.each do |key, value|
78
+ hash[key] = value
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,17 @@
1
+ monkeypatch = <<-EORUBY
2
+ class Node
3
+ def safe_transform
4
+ return unsafe_transform if SafeYAML::OPTIONS[:whitelisted_tags].include?(self.type_id)
5
+ SafeYAML::SyckResolver.new.resolve_node(self)
6
+ end
7
+
8
+ alias_method :unsafe_transform, :transform
9
+ alias_method :transform, :safe_transform
10
+ end
11
+ EORUBY
12
+
13
+ if defined?(YAML::Syck::Node)
14
+ YAML::Syck.module_eval monkeypatch
15
+ else
16
+ Syck.module_eval monkeypatch
17
+ end
@@ -1,49 +1,35 @@
1
1
  module SafeYAML
2
- class SyckResolver
3
- QUOTE_STYLES = [:quote1, :quote2]
2
+ class SyckResolver < Resolver
3
+ QUOTE_STYLES = [:quote1, :quote2].freeze
4
4
 
5
- def resolve_node(node)
6
- case node.value
7
- when Hash
8
- return resolve_map(node)
9
- when Array
10
- return resolve_seq(node)
11
- when String
12
- return resolve_scalar(node)
13
- else
14
- raise "Don't know how to resolve this node: #{node.inspect}"
15
- end
16
- end
17
-
18
- def resolve_map(node)
19
- map = node.value
5
+ NODE_TYPES = {
6
+ Hash => :map,
7
+ Array => :seq,
8
+ String => :scalar
9
+ }.freeze
20
10
 
21
- hash = {}
11
+ def initialize
12
+ super()
13
+ end
22
14
 
23
- # Take the "<<" key nodes first, as these are meant to approximate a form of inheritance.
24
- inheritors = map.keys.select { |node| resolve_node(node) == "<<" }
25
- inheritors.each do |key_node|
26
- value_node = map[key_node]
27
- hash.merge!(resolve_node(value_node))
28
- end
15
+ def native_resolve(node)
16
+ node.transform
17
+ end
29
18
 
30
- # All that's left should be normal (non-"<<") nodes.
31
- normal_keys = map.keys.reject { |node| resolve_node(node) == "<<" }
32
- normal_keys.each do |key_node|
33
- value_node = map[key_node]
34
- hash[resolve_node(key_node)] = resolve_node(value_node)
35
- end
19
+ def get_node_type(node)
20
+ NODE_TYPES[node.value.class]
21
+ end
36
22
 
37
- return hash
23
+ def get_node_tag(node)
24
+ node.type_id
38
25
  end
39
26
 
40
- def resolve_seq(node)
41
- seq = node.value
42
- seq.map { |node| resolve_node(node) }
27
+ def get_node_value(node)
28
+ node.value
43
29
  end
44
30
 
45
- def resolve_scalar(node)
46
- Transform.to_proper_type(node.value, QUOTE_STYLES.include?(node.instance_variable_get(:@style)), node.type_id)
31
+ def value_is_quoted?(node)
32
+ QUOTE_STYLES.include?(node.instance_variable_get(:@style))
47
33
  end
48
34
  end
49
35
  end
@@ -24,15 +24,13 @@ module SafeYAML
24
24
 
25
25
  value
26
26
  end
27
-
28
- def self.to_proper_type(value, quoted=false,tag=nil)
27
+
28
+ def self.to_proper_type(value, quoted=false, tag=nil)
29
29
  case tag
30
- when "tag:yaml.org,2002:binary"
31
- return Base64.decode64(value)
32
- when "x-private:binary"
33
- return Base64.decode64(value)
30
+ when "tag:yaml.org,2002:binary", "x-private:binary", "!binary"
31
+ Base64.decode64(value)
34
32
  else
35
- return self.to_guessed_type(value, quoted)
33
+ self.to_guessed_type(value, quoted)
36
34
  end
37
35
  end
38
36
  end
@@ -4,7 +4,7 @@ module SafeYAML
4
4
  MATCHER = /\A:\w+\Z/.freeze
5
5
 
6
6
  def transform?(value)
7
- return false unless SafeYAML::OPTIONS[:enable_symbol_parsing] && MATCHER.match(value)
7
+ return false unless SafeYAML::OPTIONS[:deserialize_symbols] && MATCHER.match(value)
8
8
  return true, value[1..-1].to_sym
9
9
  end
10
10
  end
@@ -1,3 +1,3 @@
1
1
  module SafeYAML
2
- VERSION = "0.7.1"
2
+ VERSION = "0.8.0"
3
3
  end
@@ -0,0 +1,10 @@
1
+ require File.join(File.dirname(__FILE__), "spec_helper")
2
+
3
+ if SafeYAML::YAML_ENGINE == "psych"
4
+ require "safe_yaml/psych_resolver"
5
+
6
+ describe SafeYAML::PsychResolver do
7
+ include ResolverSpecs
8
+ let(:resolver) { SafeYAML::PsychResolver.new }
9
+ end
10
+ end
@@ -1,6 +1,14 @@
1
- module SharedSpecs
1
+ module ResolverSpecs
2
2
  def self.included(base)
3
- base.instance_eval do
3
+ base.module_eval do
4
+ let(:resolver) { nil }
5
+ let(:result) { @result }
6
+
7
+ def parse(yaml)
8
+ tree = YAML.parse(yaml.unindent)
9
+ @result = resolver.resolve_node(tree)
10
+ end
11
+
4
12
  context "by default" do
5
13
  it "translates maps to hashes" do
6
14
  parse <<-YAML
@@ -190,13 +198,13 @@ module SharedSpecs
190
198
  end
191
199
  end
192
200
 
193
- context "with symbol parsing enabled" do
201
+ context "with symbol deserialization enabled" do
194
202
  before :each do
195
- YAML.enable_symbol_parsing!
203
+ SafeYAML::OPTIONS[:deserialize_symbols] = true
196
204
  end
197
205
 
198
206
  after :each do
199
- YAML.disable_symbol_parsing!
207
+ SafeYAML.reset_defaults!
200
208
  end
201
209
 
202
210
  it "translates values starting with ':' to symbols" do
@@ -12,7 +12,7 @@ describe YAML do
12
12
  end
13
13
 
14
14
  before :each do
15
- YAML.disable_symbol_parsing!
15
+ SafeYAML::OPTIONS[:deserialize_symbols] = false
16
16
  end
17
17
 
18
18
  describe "unsafe_load" do
@@ -32,6 +32,32 @@ describe YAML do
32
32
  backdoor = YAML.unsafe_load("--- !ruby/object:ExploitableBackDoor\nfoo: bar\n")
33
33
  backdoor.should be_exploited_through_ivars
34
34
  end
35
+
36
+ context "with special whitelisted tags defined" do
37
+ 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
43
+ end
44
+
45
+ after :each do
46
+ SafeYAML.reset_defaults!
47
+ end
48
+
49
+ it "effectively ignores the whitelist (since everything is whitelisted)" do
50
+ result = YAML.unsafe_load <<-YAML.unindent
51
+ --- !ruby/object:OpenStruct
52
+ table:
53
+ :backdoor: !ruby/object:ExploitableBackDoor
54
+ foo: bar
55
+ YAML
56
+
57
+ result.should be_a(OpenStruct)
58
+ result.backdoor.should be_exploited_through_ivars
59
+ end
60
+ end
35
61
  end
36
62
 
37
63
  describe "safe_load" do
@@ -54,12 +80,11 @@ describe YAML do
54
80
  ["foo: bar"]
55
81
  end
56
82
  }
83
+
57
84
  it "uses Psych internally to parse YAML" do
58
- stub_parser = stub(Psych::Parser)
59
- Psych::Parser.stub(:new).and_return(stub_parser)
60
- stub_parser.should_receive(:parse).with(*arguments)
85
+ Psych.should_receive(:parse).with(*arguments)
61
86
  # This won't work now; we just want to ensure Psych::Parser#parse was in fact called.
62
- YAML.safe_load(*arguments)
87
+ YAML.safe_load(*arguments) rescue nil
63
88
  end
64
89
  end
65
90
 
@@ -127,6 +152,17 @@ describe YAML do
127
152
  result.should == {"foo" => "bar", "bar" => "baz"}
128
153
  end
129
154
 
155
+ it "works for YAML documents with binary tagged array values" do
156
+ result = YAML.safe_load <<-YAML
157
+ - !binary |-
158
+ Zm9v
159
+ - !binary |-
160
+ YmFy
161
+ YAML
162
+
163
+ result.should == ["foo", "bar"]
164
+ end
165
+
130
166
  it "works for YAML documents with sections" do
131
167
  result = YAML.safe_load <<-YAML
132
168
  mysql: &mysql
@@ -203,6 +239,87 @@ describe YAML do
203
239
  "grandcustom" => { "foo" => "foo", "bar" => "custom_bar", "baz" => "custom_baz" }
204
240
  }
205
241
  end
242
+
243
+ context "with custom initializers defined" do
244
+ before :each do
245
+ if SafeYAML::YAML_ENGINE == "psych"
246
+ SafeYAML::OPTIONS[:custom_initializers] = {
247
+ "!set" => lambda { Set.new },
248
+ "!hashiemash" => lambda { Hashie::Mash.new }
249
+ }
250
+ else
251
+ SafeYAML::OPTIONS[:custom_initializers] = {
252
+ "tag:yaml.org,2002:set" => lambda { Set.new },
253
+ "tag:yaml.org,2002:hashiemash" => lambda { Hashie::Mash.new }
254
+ }
255
+ end
256
+ end
257
+
258
+ after :each do
259
+ SafeYAML.reset_defaults!
260
+ end
261
+
262
+ it "will use a custom initializer to instantiate an array-like class upon deserialization" do
263
+ result = YAML.safe_load <<-YAML.unindent
264
+ --- !set
265
+ - 1
266
+ - 2
267
+ - 3
268
+ YAML
269
+
270
+ result.should be_a(Set)
271
+ result.to_a.should =~ [1, 2, 3]
272
+ end
273
+
274
+ it "will use a custom initializer to instantiate a hash-like class upon deserialization" do
275
+ result = YAML.safe_load <<-YAML.unindent
276
+ --- !hashiemash
277
+ foo: bar
278
+ YAML
279
+
280
+ result.should be_a(Hashie::Mash)
281
+ result.to_hash.should == { "foo" => "bar" }
282
+ end
283
+ end
284
+
285
+ context "with special whitelisted tags defined" do
286
+ before :each do
287
+ if SafeYAML::YAML_ENGINE == "psych"
288
+ SafeYAML::OPTIONS[:whitelisted_tags] = ["!ruby/object:OpenStruct"]
289
+ else
290
+ SafeYAML::OPTIONS[:whitelisted_tags] = ["tag:ruby.yaml.org,2002:object:OpenStruct"]
291
+ end
292
+ end
293
+
294
+ after :each do
295
+ SafeYAML.reset_defaults!
296
+ end
297
+
298
+ it "will allow objects to be deserialized for whitelisted tags" do
299
+ result = YAML.safe_load("--- !ruby/object:OpenStruct\ntable:\n foo: bar\n")
300
+ result.should be_a(OpenStruct)
301
+ result.instance_variable_get(:@table).should == { "foo" => "bar" }
302
+ end
303
+
304
+ it "will not deserialize objects without whitelisted tags" do
305
+ result = YAML.safe_load("--- !ruby/hash:ExploitableBackDoor\nfoo: bar\n")
306
+ result.should_not be_a(ExploitableBackDoor)
307
+ result.should == { "foo" => "bar" }
308
+ end
309
+
310
+ it "will not allow non-whitelisted objects to be embedded within objects with whitelisted tags" do
311
+ result = YAML.safe_load <<-YAML.unindent
312
+ --- !ruby/object:OpenStruct
313
+ table:
314
+ :backdoor: !ruby/object:ExploitableBackDoor
315
+ foo: bar
316
+ YAML
317
+
318
+ result.should be_a(OpenStruct)
319
+ result.backdoor.should_not be_a(ExploitableBackDoor)
320
+ result.instance_variable_get(:@table).should == { ":backdoor" => { "foo" => "bar" } }
321
+ end
322
+ end
206
323
  end
207
324
 
208
325
  describe "unsafe_load_file" do
@@ -247,12 +364,19 @@ describe YAML do
247
364
  end
248
365
  }
249
366
 
250
- context "with :suppress_warnings set to true" do
251
- before :each do SafeYAML::OPTIONS[:suppress_warnings] = true; end
252
- after :each do SafeYAML::OPTIONS[:suppress_warnings] = false; end
367
+ context "as long as a :default_mode has been specified" do
368
+ after :each do
369
+ SafeYAML.reset_defaults!
370
+ end
371
+
372
+ it "doesn't issue a warning for safe mode, since an explicit mode has been set" do
373
+ SafeYAML::OPTIONS[:default_mode] = :safe
374
+ Kernel.should_not_receive(:warn)
375
+ YAML.load(*arguments)
376
+ end
253
377
 
254
- it "doesn't issue a warning if :suppress_warnings option is set to true" do
255
- SafeYAML::OPTIONS[:suppress_warnings] = true
378
+ it "doesn't issue a warning for unsafe mode, since an explicit mode has been set" do
379
+ SafeYAML::OPTIONS[:default_mode] = :unsafe
256
380
  Kernel.should_not_receive(:warn)
257
381
  YAML.load(*arguments)
258
382
  end
@@ -289,11 +413,11 @@ describe YAML do
289
413
 
290
414
  context "with arbitrary object deserialization enabled by default" do
291
415
  before :each do
292
- YAML.enable_arbitrary_object_deserialization!
416
+ SafeYAML::OPTIONS[:default_mode] = :unsafe
293
417
  end
294
418
 
295
419
  after :each do
296
- YAML.disable_arbitrary_object_deserialization!
420
+ SafeYAML.reset_defaults!
297
421
  end
298
422
 
299
423
  it "defaults to unsafe mode if the :safe option is omitted" do
@@ -344,11 +468,11 @@ describe YAML do
344
468
 
345
469
  context "with arbitrary object deserialization enabled by default" do
346
470
  before :each do
347
- YAML.enable_arbitrary_object_deserialization!
471
+ SafeYAML::OPTIONS[:default_mode] = :unsafe
348
472
  end
349
473
 
350
474
  after :each do
351
- YAML.disable_arbitrary_object_deserialization!
475
+ SafeYAML.reset_defaults!
352
476
  end
353
477
 
354
478
  it "defaults to unsafe mode if the :safe option is omitted" do
data/spec/spec_helper.rb CHANGED
@@ -11,6 +11,8 @@ if ENV["YAMLER"] && defined?(YAML::ENGINE)
11
11
  end
12
12
 
13
13
  require "safe_yaml"
14
+ require "ostruct"
15
+ require "hashie"
14
16
  require "heredoc_unindent"
15
17
 
16
- require File.join(HERE, "shared_specs")
18
+ require File.join(HERE, "resolver_specs")
@@ -4,14 +4,7 @@ if SafeYAML::YAML_ENGINE == "syck"
4
4
  require "safe_yaml/syck_resolver"
5
5
 
6
6
  describe SafeYAML::SyckResolver do
7
+ include ResolverSpecs
7
8
  let(:resolver) { SafeYAML::SyckResolver.new }
8
- let(:result) { @result }
9
-
10
- def parse(yaml)
11
- tree = YAML.parse(yaml.unindent)
12
- @result = resolver.resolve_node(tree)
13
- end
14
-
15
- include SharedSpecs
16
9
  end
17
10
  end
@@ -1,44 +1,44 @@
1
1
  require File.join(File.dirname(__FILE__), "..", "spec_helper")
2
2
 
3
3
  describe SafeYAML::Transform::ToSymbol do
4
- def with_symbol_parsing_value(value)
5
- symbol_parsing_flag = YAML.enable_symbol_parsing?
6
- SafeYAML::OPTIONS[:enable_symbol_parsing] = value
4
+ def with_symbol_deserialization_value(value)
5
+ symbol_deserialization_flag = SafeYAML::OPTIONS[:deserialize_symbols]
6
+ SafeYAML::OPTIONS[:deserialize_symbols] = value
7
7
 
8
8
  yield
9
9
 
10
10
  ensure
11
- SafeYAML::OPTIONS[:enable_symbol_parsing] = symbol_parsing_flag
11
+ SafeYAML::OPTIONS[:deserialize_symbols] = symbol_deserialization_flag
12
12
  end
13
13
 
14
- def with_symbol_parsing(&block)
15
- with_symbol_parsing_value(true, &block)
14
+ def with_symbol_deserialization(&block)
15
+ with_symbol_deserialization_value(true, &block)
16
16
  end
17
17
 
18
- def without_symbol_parsing(&block)
19
- with_symbol_parsing_value(false, &block)
18
+ def without_symbol_deserialization(&block)
19
+ with_symbol_deserialization_value(false, &block)
20
20
  end
21
21
 
22
22
  it "returns true when the value matches a valid Symbol" do
23
- with_symbol_parsing { subject.transform?(":foo")[0].should be_true }
23
+ with_symbol_deserialization { subject.transform?(":foo")[0].should be_true }
24
24
  end
25
25
 
26
- it "returns false when symbol parsing is disabled" do
27
- without_symbol_parsing { subject.transform?(":foo").should be_false }
26
+ it "returns false when symbol deserialization is disabled" do
27
+ without_symbol_deserialization { subject.transform?(":foo").should be_false }
28
28
  end
29
29
 
30
30
  it "returns false when the value does not match a valid Symbol" do
31
- with_symbol_parsing { subject.transform?("foo").should be_false }
31
+ with_symbol_deserialization { subject.transform?("foo").should be_false }
32
32
  end
33
33
 
34
34
  it "returns false when the symbol does not begin the line" do
35
- with_symbol_parsing do
35
+ with_symbol_deserialization do
36
36
  subject.transform?("NOT A SYMBOL\n:foo").should be_false
37
37
  end
38
38
  end
39
39
 
40
40
  it "returns false when the symbol does not end the line" do
41
- with_symbol_parsing do
41
+ with_symbol_deserialization do
42
42
  subject.transform?(":foo\nNOT A SYMBOL").should be_false
43
43
  end
44
44
  end
metadata CHANGED
@@ -1,23 +1,32 @@
1
- --- !ruby/object:Gem::Specification
1
+ --- !ruby/object:Gem::Specification
2
2
  name: safe_yaml
3
- version: !ruby/object:Gem::Version
4
- version: 0.7.1
3
+ version: !ruby/object:Gem::Version
4
+ hash: 63
5
5
  prerelease:
6
+ segments:
7
+ - 0
8
+ - 8
9
+ - 0
10
+ version: 0.8.0
6
11
  platform: ruby
7
- authors:
12
+ authors:
8
13
  - Dan Tao
9
14
  autorequire:
10
15
  bindir: bin
11
16
  cert_chain: []
12
- date: 2013-02-11 00:00:00.000000000 Z
17
+
18
+ date: 2013-02-17 00:00:00 Z
13
19
  dependencies: []
14
- description: Parse YAML safely, without that pesky arbitrary object deserialization
15
- vulnerability
20
+
21
+ description: Parse YAML safely, without that pesky arbitrary object deserialization vulnerability
16
22
  email: daniel.tao@gmail.com
17
23
  executables: []
24
+
18
25
  extensions: []
26
+
19
27
  extra_rdoc_files: []
20
- files:
28
+
29
+ files:
21
30
  - .gitignore
22
31
  - .travis.yml
23
32
  - Gemfile
@@ -25,7 +34,10 @@ files:
25
34
  - README.md
26
35
  - Rakefile
27
36
  - lib/safe_yaml.rb
28
- - lib/safe_yaml/psych_handler.rb
37
+ - lib/safe_yaml/psych_resolver.rb
38
+ - lib/safe_yaml/psych_visitor.rb
39
+ - lib/safe_yaml/resolver.rb
40
+ - lib/safe_yaml/syck_node_monkeypatch.rb
29
41
  - lib/safe_yaml/syck_resolver.rb
30
42
  - lib/safe_yaml/transform.rb
31
43
  - lib/safe_yaml/transform/to_boolean.rb
@@ -40,9 +52,9 @@ files:
40
52
  - safe_yaml.gemspec
41
53
  - spec/exploit.1.9.2.yaml
42
54
  - spec/exploit.1.9.3.yaml
43
- - spec/psych_handler_spec.rb
55
+ - spec/psych_resolver_spec.rb
56
+ - spec/resolver_specs.rb
44
57
  - spec/safe_yaml_spec.rb
45
- - spec/shared_specs.rb
46
58
  - spec/spec_helper.rb
47
59
  - spec/support/exploitable_back_door.rb
48
60
  - spec/syck_resolver_spec.rb
@@ -52,37 +64,46 @@ files:
52
64
  - spec/transform/to_symbol_spec.rb
53
65
  - spec/transform/to_time_spec.rb
54
66
  homepage: http://dtao.github.com/safe_yaml/
55
- licenses:
67
+ licenses:
56
68
  - MIT
57
69
  post_install_message:
58
70
  rdoc_options: []
59
- require_paths:
71
+
72
+ require_paths:
60
73
  - lib
61
- required_ruby_version: !ruby/object:Gem::Requirement
74
+ required_ruby_version: !ruby/object:Gem::Requirement
62
75
  none: false
63
- requirements:
64
- - - '>='
65
- - !ruby/object:Gem::Version
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ hash: 57
80
+ segments:
81
+ - 1
82
+ - 8
83
+ - 7
66
84
  version: 1.8.7
67
- required_rubygems_version: !ruby/object:Gem::Requirement
85
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
86
  none: false
69
- requirements:
70
- - - '>='
71
- - !ruby/object:Gem::Version
72
- version: '0'
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ hash: 3
91
+ segments:
92
+ - 0
93
+ version: "0"
73
94
  requirements: []
95
+
74
96
  rubyforge_project:
75
97
  rubygems_version: 1.8.25
76
98
  signing_key:
77
99
  specification_version: 3
78
- summary: SameYAML provides an alternative implementation of YAML.load suitable for
79
- accepting user input in Ruby applications.
80
- test_files:
100
+ summary: SameYAML provides an alternative implementation of YAML.load suitable for accepting user input in Ruby applications.
101
+ test_files:
81
102
  - spec/exploit.1.9.2.yaml
82
103
  - spec/exploit.1.9.3.yaml
83
- - spec/psych_handler_spec.rb
104
+ - spec/psych_resolver_spec.rb
105
+ - spec/resolver_specs.rb
84
106
  - spec/safe_yaml_spec.rb
85
- - spec/shared_specs.rb
86
107
  - spec/spec_helper.rb
87
108
  - spec/support/exploitable_back_door.rb
88
109
  - spec/syck_resolver_spec.rb
@@ -1,91 +0,0 @@
1
- require "psych"
2
- require "base64"
3
-
4
- module SafeYAML
5
- class PsychHandler < Psych::Handler
6
- def initialize
7
- @anchors = {}
8
- @stack = []
9
- @current_key = nil
10
- @result = nil
11
- end
12
-
13
- def result
14
- @result
15
- end
16
-
17
- def add_to_current_structure(value, anchor=nil, quoted=nil, tag=nil)
18
- value = Transform.to_proper_type(value, quoted, tag)
19
-
20
- @anchors[anchor] = value if anchor
21
-
22
- if @result.nil?
23
- @result = value
24
- @current_structure = @result
25
- return
26
- end
27
-
28
- case @current_structure
29
- when Array
30
- @current_structure.push(value)
31
-
32
- when Hash
33
- if @current_key.nil?
34
- @current_key = value
35
-
36
- else
37
- if @current_key == "<<"
38
- @current_structure.merge!(value)
39
- else
40
- @current_structure[@current_key] = value
41
- end
42
-
43
- @current_key = nil
44
- end
45
-
46
- else
47
- raise "Don't know how to add to a #{@current_structure.class}!"
48
- end
49
- end
50
-
51
- def end_current_structure
52
- @stack.pop
53
- @current_structure = @stack.last
54
- end
55
-
56
- def streaming?
57
- false
58
- end
59
-
60
- # event handlers
61
- def alias(anchor)
62
- add_to_current_structure(@anchors[anchor])
63
- end
64
-
65
- def scalar(value, anchor, tag, plain, quoted, style)
66
- add_to_current_structure(value, anchor, quoted, tag)
67
- end
68
-
69
- def start_mapping(anchor, tag, implicit, style)
70
- map = {}
71
- self.add_to_current_structure(map, anchor)
72
- @current_structure = map
73
- @stack.push(map)
74
- end
75
-
76
- def end_mapping
77
- self.end_current_structure()
78
- end
79
-
80
- def start_sequence(anchor, tag, implicit, style)
81
- seq = []
82
- self.add_to_current_structure(seq, anchor)
83
- @current_structure = seq
84
- @stack.push(seq)
85
- end
86
-
87
- def end_sequence
88
- self.end_current_structure()
89
- end
90
- end
91
- end
@@ -1,17 +0,0 @@
1
- require File.join(File.dirname(__FILE__), "spec_helper")
2
-
3
- if SafeYAML::YAML_ENGINE == "psych"
4
- require "safe_yaml/psych_handler"
5
-
6
- describe SafeYAML::PsychHandler do
7
- let(:handler) { SafeYAML::PsychHandler.new }
8
- let(:parser) { Psych::Parser.new(handler) }
9
- let(:result) { handler.result }
10
-
11
- def parse(yaml)
12
- parser.parse(yaml.unindent)
13
- end
14
-
15
- include SharedSpecs
16
- end
17
- end