safe_yaml 0.1 → 1.0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.travis.yml +48 -0
  4. data/CHANGES.md +154 -0
  5. data/Gemfile +3 -1
  6. data/LICENSE.txt +22 -0
  7. data/README.md +191 -0
  8. data/Rakefile +22 -2
  9. data/bin/safe_yaml +75 -0
  10. data/bundle_install_all_ruby_versions.sh +11 -0
  11. data/lib/safe_yaml.rb +90 -6
  12. data/lib/safe_yaml/deep.rb +34 -0
  13. data/lib/safe_yaml/libyaml_checker.rb +36 -0
  14. data/lib/safe_yaml/load.rb +181 -0
  15. data/lib/safe_yaml/parse/date.rb +37 -0
  16. data/lib/safe_yaml/parse/hexadecimal.rb +12 -0
  17. data/lib/safe_yaml/parse/sexagesimal.rb +26 -0
  18. data/lib/safe_yaml/psych_handler.rb +99 -0
  19. data/lib/safe_yaml/psych_resolver.rb +52 -0
  20. data/lib/safe_yaml/resolver.rb +94 -0
  21. data/lib/safe_yaml/safe_to_ruby_visitor.rb +29 -0
  22. data/lib/safe_yaml/store.rb +39 -0
  23. data/lib/safe_yaml/syck_hack.rb +36 -0
  24. data/lib/safe_yaml/syck_node_monkeypatch.rb +43 -0
  25. data/lib/safe_yaml/syck_resolver.rb +38 -0
  26. data/lib/safe_yaml/transform.rb +41 -0
  27. data/lib/safe_yaml/transform/to_boolean.rb +21 -0
  28. data/lib/safe_yaml/transform/to_date.rb +13 -0
  29. data/lib/safe_yaml/transform/to_float.rb +33 -0
  30. data/lib/safe_yaml/transform/to_integer.rb +26 -0
  31. data/lib/safe_yaml/transform/to_nil.rb +18 -0
  32. data/lib/safe_yaml/transform/to_symbol.rb +17 -0
  33. data/lib/safe_yaml/transform/transformation_map.rb +47 -0
  34. data/lib/{version.rb → safe_yaml/version.rb} +1 -1
  35. data/run_specs_all_ruby_versions.sh +38 -0
  36. data/safe_yaml.gemspec +11 -8
  37. data/spec/exploit.1.9.2.yaml +2 -0
  38. data/spec/exploit.1.9.3.yaml +2 -0
  39. data/spec/issue48.txt +20 -0
  40. data/spec/issue49.yml +0 -0
  41. data/spec/libyaml_checker_spec.rb +69 -0
  42. data/spec/psych_resolver_spec.rb +10 -0
  43. data/spec/resolver_specs.rb +278 -0
  44. data/spec/safe_yaml_spec.rb +697 -23
  45. data/spec/spec_helper.rb +37 -2
  46. data/spec/store_spec.rb +57 -0
  47. data/spec/support/exploitable_back_door.rb +13 -7
  48. data/spec/syck_resolver_spec.rb +10 -0
  49. data/spec/transform/base64_spec.rb +11 -0
  50. data/spec/transform/to_date_spec.rb +60 -0
  51. data/spec/transform/to_float_spec.rb +42 -0
  52. data/spec/transform/to_integer_spec.rb +64 -0
  53. data/spec/transform/to_symbol_spec.rb +51 -0
  54. data/spec/yaml_spec.rb +15 -0
  55. metadata +78 -24
  56. data/Gemfile.lock +0 -28
  57. data/lib/handler.rb +0 -86
  58. data/spec/handler_spec.rb +0 -108
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
4
+
5
+ require 'optparse'
6
+ require 'safe_yaml/load'
7
+
8
+ options = {}
9
+ option_parser = OptionParser.new do |opts|
10
+ opts.banner = "Usage: safe_yaml [options]"
11
+
12
+ opts.on("-f", "--file=<path>", "Parse the given YAML file, dump the result to STDOUT") do |file|
13
+ options[:file] = file
14
+ end
15
+
16
+ opts.on("--libyaml-check", "Check for libyaml vulnerability CVE-2014-2525 on your system") do
17
+ options[:libyaml_check] = true
18
+ end
19
+ end
20
+
21
+ option_parser.parse!
22
+
23
+ def report_libyaml_ok
24
+ puts "\e[32mGood news! You definitely have either a patched or up-to-date libyaml version :)\e[39m"
25
+ end
26
+
27
+ def check_for_overflow_bug
28
+ YAML.load("--- !#{'%20' * 100}")
29
+ report_libyaml_ok
30
+ end
31
+
32
+ def perform_libyaml_check(force=false)
33
+ unless SafeYAML::LibyamlChecker.libyaml_version_ok?
34
+ warn <<-EOM.gsub(/^ +/, ' ')
35
+
36
+ \e[33mSafeYAML Warning\e[39m
37
+ \e[33m----------------\e[39m
38
+
39
+ \e[31mYou may have an outdated version of libyaml (#{SafeYAML::LibyamlChecker::LIBYAML_VERSION}) installed on your system.\e[39m
40
+
41
+ Prior to 0.1.6, libyaml is vulnerable to a heap overflow exploit from malicious YAML payloads.
42
+
43
+ For more info, see:
44
+ https://www.ruby-lang.org/en/news/2014/03/29/heap-overflow-in-yaml-uri-escape-parsing-cve-2014-2525/
45
+ EOM
46
+ end
47
+
48
+ puts <<-EOM.gsub(/^ +/, ' ')
49
+
50
+ Hit Enter to check if your version of libyaml is vulnerable. This will run a test \e[31mwhich may crash\e[39m
51
+ \e[31mthe current process\e[39m. If it does, your system is vulnerable and you should do something about it.
52
+
53
+ Type "nm" and hit Enter if you don't want to run the check.
54
+
55
+ See the project wiki for more info:
56
+
57
+ https://github.com/dtao/safe_yaml/wiki/The-libyaml-vulnerability
58
+ EOM
59
+
60
+ if STDIN.readline.chomp("\n") != 'nm'
61
+ check_for_overflow_bug
62
+ end
63
+ end
64
+
65
+ if options[:libyaml_check]
66
+ perform_libyaml_check(options[:force_libyaml_check])
67
+
68
+ elsif options[:file]
69
+ yaml = File.read(options[:file])
70
+ result = SafeYAML.load(yaml)
71
+ puts result.inspect
72
+
73
+ else
74
+ puts option_parser.help
75
+ end
@@ -0,0 +1,11 @@
1
+ #!/bin/bash
2
+
3
+ [[ -s "$HOME/.rvm/scripts/rvm" ]] && . "$HOME/.rvm/scripts/rvm"
4
+
5
+ declare -a versions=("1.8.7" "1.9.2" "1.9.3" "2.0.0" "2.1.0" "2.1.1" "2.1.2" "ruby-head" "jruby")
6
+
7
+ for i in "${versions[@]}"
8
+ do
9
+ rvm use $i
10
+ bundle install
11
+ done
@@ -1,10 +1,94 @@
1
- require "yaml"
2
- require "handler"
1
+ require "safe_yaml/load"
3
2
 
4
3
  module YAML
5
- def self.safe_load(yaml)
6
- safe_handler = SafeYAML::Handler.new
7
- Psych::Parser.new(safe_handler).parse(yaml)
8
- return safe_handler.result
4
+ def self.load_with_options(yaml, *original_arguments)
5
+ filename, options = filename_and_options_from_arguments(original_arguments)
6
+ safe_mode = safe_mode_from_options("load", options)
7
+ arguments = [yaml]
8
+
9
+ if safe_mode == :safe
10
+ arguments << filename if SafeYAML::YAML_ENGINE == "psych"
11
+ arguments << options_for_safe_load(options)
12
+ safe_load(*arguments)
13
+ else
14
+ arguments << filename if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
15
+ unsafe_load(*arguments)
16
+ end
17
+ end
18
+
19
+ def self.load_file_with_options(file, options={})
20
+ safe_mode = safe_mode_from_options("load_file", options)
21
+ if safe_mode == :safe
22
+ safe_load_file(file, options_for_safe_load(options))
23
+ else
24
+ unsafe_load_file(file)
25
+ end
26
+ end
27
+
28
+ def self.safe_load(*args)
29
+ SafeYAML.load(*args)
30
+ end
31
+
32
+ def self.safe_load_file(*args)
33
+ SafeYAML.load_file(*args)
34
+ end
35
+
36
+ if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
37
+ def self.unsafe_load_file(filename)
38
+ # https://github.com/tenderlove/psych/blob/v1.3.2/lib/psych.rb#L296-298
39
+ File.open(filename, 'r:bom|utf-8') { |f| self.unsafe_load(f, filename) }
40
+ end
41
+
42
+ else
43
+ def self.unsafe_load_file(filename)
44
+ # https://github.com/tenderlove/psych/blob/v1.2.2/lib/psych.rb#L231-233
45
+ self.unsafe_load File.open(filename)
46
+ end
47
+ end
48
+
49
+ class << self
50
+ alias_method :unsafe_load, :load
51
+ alias_method :load, :load_with_options
52
+ alias_method :load_file, :load_file_with_options
53
+
54
+ private
55
+ def filename_and_options_from_arguments(arguments)
56
+ if arguments.count == 1
57
+ if arguments.first.is_a?(String)
58
+ return arguments.first, {}
59
+ else
60
+ return nil, arguments.first || {}
61
+ end
62
+
63
+ else
64
+ return arguments.first, arguments.last || {}
65
+ end
66
+ end
67
+
68
+ def safe_mode_from_options(method, options={})
69
+ if options[:safe].nil?
70
+ safe_mode = SafeYAML::OPTIONS[:default_mode] || :safe
71
+
72
+ if SafeYAML::OPTIONS[:default_mode].nil? && !SafeYAML::OPTIONS[:suppress_warnings]
73
+
74
+ Kernel.warn <<-EOWARNING.gsub(/^\s+/, '')
75
+ Called '#{method}' without the :safe option -- defaulting to #{safe_mode} mode.
76
+ You can avoid this warning in the future by setting the SafeYAML::OPTIONS[:default_mode] option (to :safe or :unsafe).
77
+ EOWARNING
78
+
79
+ SafeYAML::OPTIONS[:suppress_warnings] = true
80
+ end
81
+
82
+ return safe_mode
83
+ end
84
+
85
+ options[:safe] ? :safe : :unsafe
86
+ end
87
+
88
+ def options_for_safe_load(base_options)
89
+ options = base_options.dup
90
+ options.delete(:safe)
91
+ options
92
+ end
9
93
  end
10
94
  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,36 @@
1
+ require "set"
2
+
3
+ module SafeYAML
4
+ class LibyamlChecker
5
+ LIBYAML_VERSION = Psych::LIBYAML_VERSION rescue nil
6
+
7
+ # Do proper version comparison (e.g. so 0.1.10 is >= 0.1.6)
8
+ SAFE_LIBYAML_VERSION = Gem::Version.new("0.1.6")
9
+
10
+ KNOWN_PATCHED_LIBYAML_VERSIONS = Set.new([
11
+ # http://people.canonical.com/~ubuntu-security/cve/2014/CVE-2014-2525.html
12
+ "0.1.4-2ubuntu0.12.04.3",
13
+ "0.1.4-2ubuntu0.12.10.3",
14
+ "0.1.4-2ubuntu0.13.10.3",
15
+ "0.1.4-3ubuntu3",
16
+
17
+ # https://security-tracker.debian.org/tracker/CVE-2014-2525
18
+ "0.1.3-1+deb6u4",
19
+ "0.1.4-2+deb7u4",
20
+ "0.1.4-3.2"
21
+ ]).freeze
22
+
23
+ def self.libyaml_version_ok?
24
+ return true if YAML_ENGINE != "psych" || defined?(JRUBY_VERSION)
25
+ return true if Gem::Version.new(LIBYAML_VERSION || "0") >= SAFE_LIBYAML_VERSION
26
+ return libyaml_patched?
27
+ end
28
+
29
+ def self.libyaml_patched?
30
+ return false if (`which dpkg` rescue '').empty?
31
+ libyaml_version = `dpkg -s libyaml-0-2`.match(/^Version: (.*)$/)
32
+ return false if libyaml_version.nil?
33
+ KNOWN_PATCHED_LIBYAML_VERSIONS.include?(libyaml_version[1])
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,181 @@
1
+ require "set"
2
+ require "yaml"
3
+
4
+ # This needs to be defined up front in case any internal classes need to base
5
+ # their behavior off of this.
6
+ module SafeYAML
7
+ YAML_ENGINE = defined?(YAML::ENGINE) ? YAML::ENGINE.yamler : (defined?(Psych) && YAML == Psych ? "psych" : "syck")
8
+ end
9
+
10
+ require "safe_yaml/libyaml_checker"
11
+ require "safe_yaml/deep"
12
+ require "safe_yaml/parse/hexadecimal"
13
+ require "safe_yaml/parse/sexagesimal"
14
+ require "safe_yaml/parse/date"
15
+ require "safe_yaml/transform/transformation_map"
16
+ require "safe_yaml/transform/to_boolean"
17
+ require "safe_yaml/transform/to_date"
18
+ require "safe_yaml/transform/to_float"
19
+ require "safe_yaml/transform/to_integer"
20
+ require "safe_yaml/transform/to_nil"
21
+ require "safe_yaml/transform/to_symbol"
22
+ require "safe_yaml/transform"
23
+ require "safe_yaml/resolver"
24
+ require "safe_yaml/syck_hack" if SafeYAML::YAML_ENGINE == "syck" && defined?(JRUBY_VERSION)
25
+
26
+ module SafeYAML
27
+ MULTI_ARGUMENT_YAML_LOAD = YAML.method(:load).arity != 1
28
+
29
+ DEFAULT_OPTIONS = Deep.freeze({
30
+ :default_mode => nil,
31
+ :suppress_warnings => false,
32
+ :deserialize_symbols => false,
33
+ :whitelisted_tags => [],
34
+ :custom_initializers => {},
35
+ :raise_on_unknown_tag => false
36
+ })
37
+
38
+ OPTIONS = Deep.copy(DEFAULT_OPTIONS)
39
+
40
+ PREDEFINED_TAGS = {}
41
+
42
+ if YAML_ENGINE == "syck"
43
+ YAML.tagged_classes.each do |tag, klass|
44
+ PREDEFINED_TAGS[klass] = tag
45
+ end
46
+
47
+ else
48
+ # Special tags appear to be hard-coded in Psych:
49
+ # https://github.com/tenderlove/psych/blob/v1.3.4/lib/psych/visitors/to_ruby.rb
50
+ # Fortunately, there aren't many that SafeYAML doesn't already support.
51
+ PREDEFINED_TAGS.merge!({
52
+ Exception => "!ruby/exception",
53
+ Range => "!ruby/range",
54
+ Regexp => "!ruby/regexp",
55
+ })
56
+ end
57
+
58
+ Deep.freeze(PREDEFINED_TAGS)
59
+
60
+ module_function
61
+
62
+ def restore_defaults!
63
+ OPTIONS.clear.merge!(Deep.copy(DEFAULT_OPTIONS))
64
+ end
65
+
66
+ def tag_safety_check!(tag, options)
67
+ return if tag.nil? || tag == "!"
68
+ if options[:raise_on_unknown_tag] && !options[:whitelisted_tags].include?(tag) && !tag_is_explicitly_trusted?(tag)
69
+ raise "Unknown YAML tag '#{tag}'"
70
+ end
71
+ end
72
+
73
+ def whitelist!(*classes)
74
+ classes.each do |klass|
75
+ whitelist_class!(klass)
76
+ end
77
+ end
78
+
79
+ def whitelist_class!(klass)
80
+ raise "#{klass} not a Class" unless klass.is_a?(::Class)
81
+
82
+ klass_name = klass.name
83
+ raise "#{klass} cannot be anonymous" if klass_name.nil? || klass_name.empty?
84
+
85
+ # Whitelist any built-in YAML tags supplied by Syck or Psych.
86
+ predefined_tag = PREDEFINED_TAGS[klass]
87
+ if predefined_tag
88
+ OPTIONS[:whitelisted_tags] << predefined_tag
89
+ return
90
+ end
91
+
92
+ # Exception is exceptional (har har).
93
+ tag_class = klass < Exception ? "exception" : "object"
94
+
95
+ tag_prefix = case YAML_ENGINE
96
+ when "psych" then "!ruby/#{tag_class}"
97
+ when "syck" then "tag:ruby.yaml.org,2002:#{tag_class}"
98
+ else raise "unknown YAML_ENGINE #{YAML_ENGINE}"
99
+ end
100
+ OPTIONS[:whitelisted_tags] << "#{tag_prefix}:#{klass_name}"
101
+ end
102
+
103
+ if YAML_ENGINE == "psych"
104
+ def tag_is_explicitly_trusted?(tag)
105
+ false
106
+ end
107
+
108
+ else
109
+ TRUSTED_TAGS = Set.new([
110
+ "tag:yaml.org,2002:binary",
111
+ "tag:yaml.org,2002:bool#no",
112
+ "tag:yaml.org,2002:bool#yes",
113
+ "tag:yaml.org,2002:float",
114
+ "tag:yaml.org,2002:float#fix",
115
+ "tag:yaml.org,2002:int",
116
+ "tag:yaml.org,2002:map",
117
+ "tag:yaml.org,2002:null",
118
+ "tag:yaml.org,2002:seq",
119
+ "tag:yaml.org,2002:str",
120
+ "tag:yaml.org,2002:timestamp",
121
+ "tag:yaml.org,2002:timestamp#ymd"
122
+ ]).freeze
123
+
124
+ def tag_is_explicitly_trusted?(tag)
125
+ TRUSTED_TAGS.include?(tag)
126
+ end
127
+ end
128
+
129
+ if SafeYAML::YAML_ENGINE == "psych"
130
+ require "safe_yaml/psych_handler"
131
+ require "safe_yaml/psych_resolver"
132
+ require "safe_yaml/safe_to_ruby_visitor"
133
+
134
+ def self.load(yaml, filename=nil, options={})
135
+ # If the user hasn't whitelisted any tags, we can go with this implementation which is
136
+ # significantly faster.
137
+ if (options && options[:whitelisted_tags] || SafeYAML::OPTIONS[:whitelisted_tags]).empty?
138
+ safe_handler = SafeYAML::PsychHandler.new(options) do |result|
139
+ return result
140
+ end
141
+ arguments_for_parse = [yaml]
142
+ arguments_for_parse << filename if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
143
+ Psych::Parser.new(safe_handler).parse(*arguments_for_parse)
144
+ return safe_handler.result
145
+
146
+ else
147
+ safe_resolver = SafeYAML::PsychResolver.new(options)
148
+ tree = SafeYAML::MULTI_ARGUMENT_YAML_LOAD ?
149
+ Psych.parse(yaml, filename) :
150
+ Psych.parse(yaml)
151
+ return safe_resolver.resolve_node(tree)
152
+ end
153
+ end
154
+
155
+ def self.load_file(filename, options={})
156
+ if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
157
+ File.open(filename, 'r:bom|utf-8') { |f| self.load(f, filename, options) }
158
+
159
+ else
160
+ # Ruby pukes on 1.9.2 if we try to open an empty file w/ 'r:bom|utf-8';
161
+ # so we'll not specify those flags here. This mirrors the behavior for
162
+ # unsafe_load_file so it's probably preferable anyway.
163
+ self.load File.open(filename), nil, options
164
+ end
165
+ end
166
+
167
+ else
168
+ require "safe_yaml/syck_resolver"
169
+ require "safe_yaml/syck_node_monkeypatch"
170
+
171
+ def self.load(yaml, options={})
172
+ resolver = SafeYAML::SyckResolver.new(SafeYAML::OPTIONS.merge(options || {}))
173
+ tree = YAML.parse(yaml)
174
+ return resolver.resolve_node(tree)
175
+ end
176
+
177
+ def self.load_file(filename, options={})
178
+ File.open(filename) { |f| self.load(f, options) }
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,37 @@
1
+ require 'time'
2
+
3
+ module SafeYAML
4
+ class Parse
5
+ class Date
6
+ # This one's easy enough :)
7
+ DATE_MATCHER = /\A(\d{4})-(\d{2})-(\d{2})\Z/.freeze
8
+
9
+ # This unbelievable little gem is taken basically straight from the YAML spec, but made
10
+ # slightly more readable (to my poor eyes at least) to me:
11
+ # http://yaml.org/type/timestamp.html
12
+ TIME_MATCHER = /\A\d{4}-\d{1,2}-\d{1,2}(?:[Tt]|\s+)\d{1,2}:\d{2}:\d{2}(?:\.\d*)?\s*(?:Z|[-+]\d{1,2}(?::?\d{2})?)?\Z/.freeze
13
+
14
+ SECONDS_PER_DAY = 60 * 60 * 24
15
+ MICROSECONDS_PER_SECOND = 1000000
16
+
17
+ # So this is weird. In Ruby 1.8.7, the DateTime#sec_fraction method returned fractional
18
+ # seconds in units of DAYS for some reason. In 1.9.2, they changed the units -- much more
19
+ # reasonably -- to seconds.
20
+ SEC_FRACTION_MULTIPLIER = RUBY_VERSION == "1.8.7" ? (SECONDS_PER_DAY * MICROSECONDS_PER_SECOND) : MICROSECONDS_PER_SECOND
21
+
22
+ # The DateTime class has a #to_time method in Ruby 1.9+;
23
+ # Before that we'll just need to convert DateTime to Time ourselves.
24
+ TO_TIME_AVAILABLE = DateTime.instance_methods.include?(:to_time)
25
+
26
+ def self.value(value)
27
+ d = DateTime.parse(value)
28
+
29
+ return d.to_time if TO_TIME_AVAILABLE
30
+
31
+ usec = d.sec_fraction * SEC_FRACTION_MULTIPLIER
32
+ time = Time.utc(d.year, d.month, d.day, d.hour, d.min, d.sec, usec) - (d.offset * SECONDS_PER_DAY)
33
+ time.getlocal
34
+ end
35
+ end
36
+ end
37
+ end