safe_yaml-instructure 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +2 -0
- data/.travis.yml +45 -0
- data/Gemfile +3 -0
- data/LICENSE.txt +22 -0
- data/README.md +124 -0
- data/Rakefile +6 -0
- data/lib/safe_yaml.rb +145 -0
- data/lib/safe_yaml/psych_tag_verifier.rb +38 -0
- data/lib/safe_yaml/syck_tag_verifier.rb +27 -0
- data/lib/safe_yaml/tag_verifier.rb +23 -0
- data/lib/safe_yaml/version.rb +3 -0
- data/lib/safe_yaml/whitelist.rb +74 -0
- data/run_specs_all_ruby_versions.sh +21 -0
- data/safe_yaml.gemspec +24 -0
- data/spec/exploit.1.9.2.yaml +2 -0
- data/spec/exploit.1.9.3.yaml +2 -0
- data/spec/ok.yaml +3 -0
- data/spec/safe_yaml_spec.rb +541 -0
- data/spec/spec_helper.rb +15 -0
- data/spec/support/exploitable_back_door.rb +29 -0
- data/spec/whitelist_spec.rb +46 -0
- data/spec/yaml_load_spec.rb +150 -0
- metadata +166 -0
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
language:
|
2
|
+
ruby
|
3
|
+
|
4
|
+
before_install:
|
5
|
+
gem install bundler
|
6
|
+
|
7
|
+
script:
|
8
|
+
bundle exec rake spec
|
9
|
+
|
10
|
+
rvm:
|
11
|
+
- ruby-head
|
12
|
+
- 2.0.0
|
13
|
+
- 1.9.3
|
14
|
+
- 1.9.2
|
15
|
+
- 1.8.7
|
16
|
+
- rbx-19mode
|
17
|
+
- rbx-18mode
|
18
|
+
- jruby-head
|
19
|
+
- jruby-19mode
|
20
|
+
- jruby-18mode
|
21
|
+
- ree
|
22
|
+
|
23
|
+
env:
|
24
|
+
- YAMLER=syck
|
25
|
+
- YAMLER=psych
|
26
|
+
|
27
|
+
matrix:
|
28
|
+
allow_failures:
|
29
|
+
- rvm: ruby-head
|
30
|
+
- rvm: rbx-19mode
|
31
|
+
- rvm: rbx-18mode
|
32
|
+
- rvm: jruby-head
|
33
|
+
- rvm: jruby-19mode
|
34
|
+
- rvm: jruby-18mode
|
35
|
+
- rvm: ree
|
36
|
+
|
37
|
+
exclude:
|
38
|
+
- rvm: 1.8.7
|
39
|
+
env: YAMLER=psych
|
40
|
+
- rvm: jruby-head
|
41
|
+
env: YAMLER=syck
|
42
|
+
- rvm: jruby-19mode
|
43
|
+
env: YAMLER=syck
|
44
|
+
- rvm: jruby-18mode
|
45
|
+
env: YAMLER=syck
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Dan Tao
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
SafeYAML
|
2
|
+
========
|
3
|
+
|
4
|
+
[![Build Status](https://travis-ci.org/instructure/safe_yaml.png)](http://travis-ci.org/instructure/safe_yaml)
|
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))).
|
7
|
+
|
8
|
+
Installation
|
9
|
+
------------
|
10
|
+
|
11
|
+
Add this line to your application's Gemfile:
|
12
|
+
|
13
|
+
gem "safe_yaml-instructure"
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
$ bundle
|
18
|
+
|
19
|
+
Or install it yourself as:
|
20
|
+
|
21
|
+
$ gem install safe_yaml-instructure
|
22
|
+
|
23
|
+
Purpose
|
24
|
+
-------
|
25
|
+
|
26
|
+
Suppose your application were to contain some code like this:
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
class ExploitableClassBuilder
|
30
|
+
def []=(key, value)
|
31
|
+
@class ||= Class.new
|
32
|
+
|
33
|
+
@class.class_eval <<-EOS
|
34
|
+
def #{key}
|
35
|
+
#{value}
|
36
|
+
end
|
37
|
+
EOS
|
38
|
+
end
|
39
|
+
|
40
|
+
def create
|
41
|
+
@class.new
|
42
|
+
end
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
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.
|
47
|
+
|
48
|
+
Observe:
|
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"
|
55
|
+
|
56
|
+
> YAML.load(yaml)
|
57
|
+
I'm in yr system!
|
58
|
+
=> #<ExploitableClassBuilder:0x007fdbbe2e25d8 @class=#<Class:0x007fdbbe2e2510>>
|
59
|
+
|
60
|
+
With SafeYAML, that attacker would be thwarted:
|
61
|
+
|
62
|
+
> require "safe_yaml"
|
63
|
+
=> true
|
64
|
+
> YAML.load(yaml)
|
65
|
+
SafeYAML::UnsafeTagError: YAML tag is not whitelisted: tag:ruby.yaml.org,2002:object:ExploitableClassBuilder
|
66
|
+
|
67
|
+
Usage
|
68
|
+
-----
|
69
|
+
|
70
|
+
`YAML.safe_load` will load YAML without allowing arbitrary object deserialization.
|
71
|
+
|
72
|
+
`YAML.unsafe_load` will exhibit Ruby's built-in behavior: to allow the deserialization of arbitrary objects.
|
73
|
+
|
74
|
+
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
|
+
# Ruby >= 1.9.3
|
77
|
+
YAML.load(yaml, filename, :safe => true) # calls safe_load
|
78
|
+
YAML.load(yaml, filename, :safe => false) # calls unsafe_load
|
79
|
+
|
80
|
+
# Ruby < 1.9.3
|
81
|
+
YAML.load(yaml, :safe => true) # calls safe_load
|
82
|
+
YAML.load(yaml, :safe => false) # calls unsafe_load
|
83
|
+
|
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).
|
85
|
+
|
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.
|
87
|
+
|
88
|
+
Notes
|
89
|
+
-----
|
90
|
+
|
91
|
+
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
|
+
|
93
|
+
- Hashes
|
94
|
+
- Arrays
|
95
|
+
- Strings
|
96
|
+
- Numbers
|
97
|
+
- Dates
|
98
|
+
- Times
|
99
|
+
- Booleans
|
100
|
+
- Nils
|
101
|
+
|
102
|
+
Additionally, deserialization of symbols can be enabled by calling `YAML.enable_symbol_parsing!` (for example, in an initializer).
|
103
|
+
|
104
|
+
Known Issues
|
105
|
+
------------
|
106
|
+
|
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:
|
108
|
+
|
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.
|
111
|
+
|
112
|
+
The above list will grow over time, as more issues are discovered.
|
113
|
+
|
114
|
+
Caveat
|
115
|
+
------
|
116
|
+
|
117
|
+
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.
|
118
|
+
|
119
|
+
Requirements
|
120
|
+
------------
|
121
|
+
|
122
|
+
SafeYAML requires Ruby 1.8.7 or newer and works with both [Syck](http://www.ruby-doc.org/stdlib-1.8.7/libdoc/yaml/rdoc/YAML.html) and [Psych](http://github.com/tenderlove/psych).
|
123
|
+
|
124
|
+
If you are using a version of Ruby where Psych is the default YAML engine (e.g., 1.9.3) but you want to use Syck, be sure to set `YAML::ENGINE.yamler = "syck"` **before** requiring the safe_yaml gem.
|
data/Rakefile
ADDED
data/lib/safe_yaml.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
require "set"
|
2
|
+
require "yaml"
|
3
|
+
|
4
|
+
require "safe_yaml/tag_verifier"
|
5
|
+
require "safe_yaml/version"
|
6
|
+
require "safe_yaml/whitelist"
|
7
|
+
|
8
|
+
module SafeYAML
|
9
|
+
MULTI_ARGUMENT_YAML_LOAD = YAML.method(:load).arity != 1
|
10
|
+
YAML_ENGINE = defined?(YAML::ENGINE) ? YAML::ENGINE.yamler : "syck"
|
11
|
+
|
12
|
+
OPTIONS = {
|
13
|
+
:enable_symbol_parsing => false,
|
14
|
+
:enable_arbitrary_object_deserialization => false,
|
15
|
+
:suppress_warnings => false
|
16
|
+
}
|
17
|
+
|
18
|
+
class UnsafeTagError < RuntimeError; end
|
19
|
+
end
|
20
|
+
|
21
|
+
module YAML
|
22
|
+
def self.load_with_options(yaml, *filename_and_options)
|
23
|
+
options = filename_and_options.last || {}
|
24
|
+
safe_mode = safe_mode_from_options("load", options)
|
25
|
+
arguments = [yaml]
|
26
|
+
arguments << filename_and_options.first if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
|
27
|
+
safe_mode ? safe_load(*arguments) : unsafe_load(*arguments)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.load_file_with_options(file, options={})
|
31
|
+
safe_mode = safe_mode_from_options("load_file", options)
|
32
|
+
safe_mode ? safe_load_file(file) : unsafe_load_file(file)
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.read_for_safe_load(yaml)
|
36
|
+
# since we're going to do two passes, we need to read out the file here
|
37
|
+
# into a string
|
38
|
+
if yaml.respond_to?(:read)
|
39
|
+
yaml = yaml.read
|
40
|
+
end
|
41
|
+
yaml
|
42
|
+
end
|
43
|
+
|
44
|
+
if SafeYAML::YAML_ENGINE == "psych"
|
45
|
+
require "safe_yaml/psych_tag_verifier"
|
46
|
+
def self.safe_load(yaml, filename=nil)
|
47
|
+
yaml = read_for_safe_load(yaml)
|
48
|
+
verifier = SafeYAML::PsychTagVerifier.new(whitelist)
|
49
|
+
parser = Psych::Parser.new(verifier)
|
50
|
+
if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
|
51
|
+
parser.parse(yaml, filename)
|
52
|
+
else
|
53
|
+
parser.parse(yaml)
|
54
|
+
end
|
55
|
+
return unsafe_load(yaml)
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.safe_load_file(filename)
|
59
|
+
File.open(filename, 'r:bom|utf-8') { |f| self.safe_load f, filename }
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.unsafe_load_file(filename)
|
63
|
+
if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
|
64
|
+
# https://github.com/tenderlove/psych/blob/v1.3.2/lib/psych.rb#L296-298
|
65
|
+
File.open(filename, 'r:bom|utf-8') { |f| self.unsafe_load f, filename }
|
66
|
+
else
|
67
|
+
# https://github.com/tenderlove/psych/blob/v1.2.2/lib/psych.rb#L231-233
|
68
|
+
self.unsafe_load File.open(filename)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
else
|
73
|
+
require "safe_yaml/syck_tag_verifier"
|
74
|
+
def self.safe_load(yaml)
|
75
|
+
yaml = read_for_safe_load(yaml)
|
76
|
+
tree = YAML.parse(yaml)
|
77
|
+
verifier = SafeYAML::SyckTagVerifier.new(whitelist)
|
78
|
+
verifier.verify(tree)
|
79
|
+
return unsafe_load(yaml)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.safe_load_file(filename)
|
83
|
+
File.open(filename) { |f| self.safe_load f }
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.unsafe_load_file(filename)
|
87
|
+
# https://github.com/indeyets/syck/blob/master/ext/ruby/lib/yaml.rb#L133-135
|
88
|
+
File.open(filename) { |f| self.unsafe_load f }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class << self
|
93
|
+
alias_method :unsafe_load, :load
|
94
|
+
alias_method :load, :load_with_options
|
95
|
+
alias_method :load_file, :load_file_with_options
|
96
|
+
|
97
|
+
def enable_symbol_parsing?
|
98
|
+
SafeYAML::OPTIONS[:enable_symbol_parsing]
|
99
|
+
end
|
100
|
+
|
101
|
+
def enable_symbol_parsing!
|
102
|
+
SafeYAML::OPTIONS[:enable_symbol_parsing] = true
|
103
|
+
end
|
104
|
+
|
105
|
+
def disable_symbol_parsing!
|
106
|
+
SafeYAML::OPTIONS[:enable_symbol_parsing] = false
|
107
|
+
end
|
108
|
+
|
109
|
+
def enable_arbitrary_object_deserialization?
|
110
|
+
SafeYAML::OPTIONS[:enable_arbitrary_object_deserialization]
|
111
|
+
end
|
112
|
+
|
113
|
+
def enable_arbitrary_object_deserialization!
|
114
|
+
SafeYAML::OPTIONS[:enable_arbitrary_object_deserialization] = true
|
115
|
+
end
|
116
|
+
|
117
|
+
def disable_arbitrary_object_deserialization!
|
118
|
+
SafeYAML::OPTIONS[:enable_arbitrary_object_deserialization] = false
|
119
|
+
end
|
120
|
+
|
121
|
+
def whitelist
|
122
|
+
@whitelist ||= SafeYAML::Whitelist.new
|
123
|
+
end
|
124
|
+
|
125
|
+
SYMBOL_REGEX = /\A:\w+\Z/.freeze
|
126
|
+
def check_string_for_symbol!(string)
|
127
|
+
if !YAML.enable_symbol_parsing? && string.match(SYMBOL_REGEX)
|
128
|
+
raise SafeYAML::UnsafeTagError.new("Symbol parsing is disabled")
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
def safe_mode_from_options(method, options={})
|
134
|
+
safe_mode = options[:safe]
|
135
|
+
|
136
|
+
if safe_mode.nil?
|
137
|
+
mode = SafeYAML::OPTIONS[:enable_arbitrary_object_deserialization] ? "unsafe" : "safe"
|
138
|
+
Kernel.warn "Called '#{method}' without the :safe option -- defaulting to #{mode} mode." unless SafeYAML::OPTIONS[:suppress_warnings]
|
139
|
+
safe_mode = !SafeYAML::OPTIONS[:enable_arbitrary_object_deserialization]
|
140
|
+
end
|
141
|
+
|
142
|
+
safe_mode
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module SafeYAML
|
2
|
+
class PsychTagVerifier < Psych::Handler
|
3
|
+
attr_reader :tags
|
4
|
+
|
5
|
+
def initialize(whitelist)
|
6
|
+
@tags = Set.new
|
7
|
+
@verifier = SafeYAML::TagVerifier.new(whitelist)
|
8
|
+
end
|
9
|
+
|
10
|
+
def streaming?
|
11
|
+
false
|
12
|
+
end
|
13
|
+
|
14
|
+
def alias(anchor)
|
15
|
+
end
|
16
|
+
|
17
|
+
def scalar(value, anchor, tag, plain, quoted, style)
|
18
|
+
if !quoted && value.is_a?(String)
|
19
|
+
YAML.check_string_for_symbol!(value)
|
20
|
+
end
|
21
|
+
@verifier.verify_tag!(tag, value)
|
22
|
+
end
|
23
|
+
|
24
|
+
def start_mapping(anchor, tag, implicit, style)
|
25
|
+
@verifier.verify_tag!(tag, nil)
|
26
|
+
end
|
27
|
+
|
28
|
+
def end_mapping
|
29
|
+
end
|
30
|
+
|
31
|
+
def start_sequence(anchor, tag, implicit, style)
|
32
|
+
@verifier.verify_tag!(tag, nil)
|
33
|
+
end
|
34
|
+
|
35
|
+
def end_sequence
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module SafeYAML
|
2
|
+
class SyckTagVerifier
|
3
|
+
QUOTE_STYLES = [:quote1, :quote2]
|
4
|
+
|
5
|
+
attr_reader :tags
|
6
|
+
|
7
|
+
def initialize(whitelist)
|
8
|
+
@tags = Set.new
|
9
|
+
@verifier = SafeYAML::TagVerifier.new(whitelist)
|
10
|
+
end
|
11
|
+
|
12
|
+
def verify(node)
|
13
|
+
return unless node.respond_to?(:type_id)
|
14
|
+
if !QUOTE_STYLES.include?(node.instance_variable_get(:@style)) && node.value.is_a?(String)
|
15
|
+
YAML.check_string_for_symbol!(node.value)
|
16
|
+
end
|
17
|
+
@verifier.verify_tag!(node.type_id, node.value)
|
18
|
+
|
19
|
+
case node.value
|
20
|
+
when Hash
|
21
|
+
node.value.each { |k,v| verify(k); verify(v) }
|
22
|
+
when Array
|
23
|
+
node.value.each { |i| verify(i) }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module SafeYAML
|
4
|
+
class TagVerifier
|
5
|
+
def initialize(whitelist)
|
6
|
+
@whitelist = whitelist
|
7
|
+
@seen = Set.new
|
8
|
+
end
|
9
|
+
|
10
|
+
def verify_tag!(tag, value)
|
11
|
+
return if !tag || @seen.include?(tag)
|
12
|
+
|
13
|
+
case @whitelist.check(tag, value)
|
14
|
+
when :cacheable
|
15
|
+
@seen << tag
|
16
|
+
when :allowed
|
17
|
+
# in the whitelist, but can't be cached (because it called a proc for yes/no)
|
18
|
+
else
|
19
|
+
raise SafeYAML::UnsafeTagError.new("YAML tag is not whitelisted: #{tag} #{value.inspect}")
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|