safe_yaml-instructure 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,2 @@
1
+ Gemfile.lock
2
+ dist/
@@ -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
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
@@ -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.
@@ -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.
@@ -0,0 +1,6 @@
1
+ require "rspec/core/rake_task"
2
+
3
+ desc "Run specs"
4
+ RSpec::Core::RakeTask.new(:spec) do |t|
5
+ t.rspec_opts = %w(--color)
6
+ end
@@ -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