Add bin and edit workflow
Gitea Actions Demo / Explore-Gitea-Actions (push) Failing after 9s

This commit is contained in:
2026-09-16 13:11:16 -06:00
parent c8ac4fcae5
commit 4cee170d66
17576 changed files with 895740 additions and 2 deletions
+3
View File
@@ -0,0 +1,3 @@
*.gem
Gemfile.lock
spec/store.yaml
+48
View File
@@ -0,0 +1,48 @@
language:
ruby
before_install:
gem install bundler
script:
bundle exec rake spec
rvm:
- ruby-head
- 2.0.0
- 1.9.3
- 1.9.2
- 1.8.7
- rbx-19mode
- rbx-18mode
- jruby-head
- jruby-19mode
- jruby-18mode
- ree
env:
- YAMLER=syck
- YAMLER=psych
matrix:
allow_failures:
- rvm: ruby-head
- rvm: rbx-19mode
- rvm: rbx-18mode
- rvm: jruby-head
- rvm: ree
exclude:
- rvm: 1.8.7
env: YAMLER=psych
- rvm: jruby-head
env: YAMLER=syck
- rvm: jruby-19mode
env: YAMLER=syck
- rvm: jruby-18mode
env: YAMLER=syck
branches:
only:
- master
+154
View File
@@ -0,0 +1,154 @@
1.0.5
-----
- fixed [#80](https://github.com/dtao/safe_yaml/issues/80): uninitialized constant DateTime
1.0.2
-----
- added warning when using Psych + an older version of libyaml
1.0.1
-----
- fixed handling for strings that look like (invalid) dates
1.0.0
-----
- updated date parsing to use local timezone
- **now requiring "safe_yaml/load" provides `SafeYAML.load` without clobbering `YAML`**
- fixed handling of empty files
- fixed some (edge case) integer parsing bugs
- fixed some JRuby-specific issues
0.9.7
-----
- made handling of document frontmatter more robust
- added more descriptive message to the warning for omitting the :safe option
0.9.6
-----
- fixed handling of files with trailing content (after closing `---`)
0.9.5
-----
- fixed permissions AGAIN
0.9.4
-----
- corrected handling of symbols
0.9.3
-----
- fixed permissions :(
0.9.2
-----
- fixed error w/ parsing "!" when whitelisting tags
- fixed parsing of the number 0 (d'oh!)
0.9.1
-----
- added Yecht support (JRuby)
- more bug fixes
0.9.0
-----
- added `whitelist!` method for easily whitelisting tags
- added support for call-specific options
- removed deprecated methods
0.8.6
-----
- fixed bug in float matcher
0.8.5
-----
- performance improvements
- made less verbose by default
- bug fixes
0.8.4
-----
- enhancements to parsing of integers, floats, and dates
- updated built-in whitelist
- more bug fixes
0.8.3
-----
- fixed exception on parsing empty document
- fixed handling of octal & hexadecimal numbers
0.8.2
-----
- bug fixes
0.8.1
-----
- added `:raise_on_unknown_tag` option
- renamed `reset_defaults!` to `restore_defaults!`
0.8
---
- added tag whitelisting
- more API changes
0.7
---
- separated YAML engine support from Ruby version
- added support for binary scalars
- numerous bug fixes and enhancements
0.6
---
- several API changes
- added `SafeYAML::OPTIONS` for specifying default behavior
0.5
---
Added support for dates
0.4
---
- efficiency improvements
- made `YAML.load` use `YAML.safe_load` by default
- made symbol deserialization optional
0.3
---
Added Syck support
0.2
---
Added support for:
- anchors & aliases
- booleans
- nils
0.1
---
Initial release
+11
View File
@@ -0,0 +1,11 @@
source "https://rubygems.org"
gemspec
group :development do
gem "hashie"
gem "heredoc_unindent"
gem "rake"
gem "rspec"
gem "travis-lint"
end
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2013 Dan Tao
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+191
View File
@@ -0,0 +1,191 @@
SafeYAML
========
[![Build Status](https://travis-ci.org/dtao/safe_yaml.png)](http://travis-ci.org/dtao/safe_yaml)
[![Gem Version](https://badge.fury.io/rb/safe_yaml.png)](http://badge.fury.io/rb/safe_yaml)
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)).
**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)!
Installation
------------
Add this line to your application's Gemfile:
```ruby
gem "safe_yaml"
```
Configuration
-------------
If *all you do* is add SafeYAML to your project, then `YAML.load` will operate in "safe" mode, which means it won't deserialize arbitrary objects. However, it will issue a warning the first time you call it because you haven't explicitly specified whether you want safe or unsafe behavior by default. To specify this behavior (e.g., in a Rails initializer):
```ruby
SafeYAML::OPTIONS[:default_mode] = :safe # or :unsafe
```
Another important option you might want to specify on startup is whether or not to allow *symbols* to be deserialized. The default setting is `false`, since symbols are not garbage collected in Ruby and so deserializing them from YAML may render your application vulnerable to a DOS (denial of service) attack. To allow symbol deserialization by default:
```ruby
SafeYAML::OPTIONS[:deserialize_symbols] = true
```
For more information on these and other options, see the "Usage" section down below.
What is this gem for, exactly?
------------------------------
Suppose your application were to use a popular open source library which contained code like this:
```ruby
class ClassBuilder
def []=(key, value)
@class ||= Class.new
@class.class_eval <<-EOS
def #{key}
#{value}
end
EOS
end
def create
@class.new
end
end
```
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.
This simple example demonstrates the vulnerability:
```ruby
yaml = <<-EOYAML
--- !ruby/hash:ClassBuilder
"foo; end; puts %(I'm in yr system!); def bar": "baz"
EOYAML
```
> YAML.load(yaml)
I'm in yr system!
=> #<ClassBuilder:0x007fdbbe2e25d8 @class=#<Class:0x007fdbbe2e2510>>
With SafeYAML, the same attacker would be thwarted:
> require "safe_yaml"
=> true
> YAML.load(yaml, :safe => true)
=> {"foo; end; puts %(I'm in yr system!); def bar"=>"baz"}
Usage
-----
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:
- for Syck and Psych prior to Ruby 1.9.3: `YAML.load(yaml, options={})`
- for Psych in 1.9.3 and later: `YAML.load(yaml, filename=nil, options={})`
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.
- `: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.
- `: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.
- `: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.
- `:raise_on_unknown_tag` (default: `false`): Represents the highest possible level of paranoia. 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.
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.
What if I don't *want* to patch `YAML`?
---------------------------------------
[Excellent question](https://github.com/dtao/safe_yaml/issues/47)! You can also get the methods `SafeYAML.load` and `SafeYAML.load_file` without touching the `YAML` module at all like this:
```ruby
require "safe_yaml/load" # instead of require "safe_yaml"
```
This way, you can use `SafeYAML.load` to parse YAML that *you* don't trust, without affecting the rest of an application (if you're developing a library, for example).
Supported Types
---------------
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:
- Hashes
- Arrays
- Strings
- Numbers
- Dates
- Times
- Booleans
- Nils
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)`.
Whitelisting Trusted Types
--------------------------
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.
The easiest way to whitelist types is by calling `SafeYAML.whitelist!`, which can accept a variable number of safe types, e.g.:
```ruby
SafeYAML.whitelist!(Foo, Bar)
```
You can also whitelist YAML *tags* via the `:whitelisted_tags` option:
```ruby
# Using Syck
SafeYAML::OPTIONS[:whitelisted_tags] = ["tag:ruby.yaml.org,2002:object:OpenStruct"]
# Using Psych
SafeYAML::OPTIONS[:whitelisted_tags] = ["!ruby/object:OpenStruct"]
```
And in case you were wondering: no, this feature will *not* allow would-be attackers to embed untrusted types within trusted types:
```ruby
yaml = <<-EOYAML
--- !ruby/object:OpenStruct
table:
:backdoor: !ruby/hash:ClassBuilder
"foo; end; puts %(I'm in yr system!); def bar": "baz"
EOYAML
```
> YAML.safe_load(yaml)
=> #<OpenStruct :backdoor={"foo; end; puts %(I'm in yr system!); def bar"=>"baz"}>
Known Issues
------------
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]`.
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:
- [**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:
1. set the `:deserialize_symbols` option to `true`,
2. whitelist some of the types in your serialized data via `SafeYAML.whitelist!` or the `:whitelisted_tags` option, or
3. both
- [**delayed_job**](https://github.com/collectiveidea/delayed_job): Uses YAML to serialize the objects on which delayed methods are invoked (with `delay`). The safest solution in this case is to use `SafeYAML.whitelist!` to whitelist the types you need to serialize.
- [**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.
- [**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.
The above list will grow over time, as more issues are discovered.
Versioning
----------
SafeYAML will follow [semantic versioning](http://semver.org/) so any updates to the first major version will maintain backwards compatability. So expect primarily bug fixes and feature enhancements (if anything!) from here on out... unless it makes sense to break the interface at some point and introduce a version 2.0, which I honestly think is unlikely.
Requirements
------------
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).
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.
+26
View File
@@ -0,0 +1,26 @@
require "rspec/core/rake_task"
desc "Run specs"
task :spec => ['spec:app', 'spec:lib']
namespace :spec do
desc "Run only specs tagged 'solo'"
RSpec::Core::RakeTask.new(:solo) do |t|
t.verbose = false
t.rspec_opts = %w(--color --tag solo)
end
desc "Run only specs tagged NOT tagged 'libraries' (for applications)"
RSpec::Core::RakeTask.new(:app) do |t|
t.verbose = false
ENV["MONKEYPATCH_YAML"] = "true"
t.rspec_opts = %w(--color --tag ~libraries)
end
desc "Run only specs tagged 'libraries'"
RSpec::Core::RakeTask.new(:lib) do |t|
t.verbose = false
ENV["MONKEYPATCH_YAML"] = "false"
t.rspec_opts = %w(--color --tag libraries)
end
end
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env ruby
$LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
require 'optparse'
require 'safe_yaml/load'
options = {}
option_parser = OptionParser.new do |opts|
opts.banner = "Usage: safe_yaml [options]"
opts.on("-f", "--file=<path>", "Parse the given YAML file, dump the result to STDOUT") do |file|
options[:file] = file
end
opts.on("--libyaml-check", "Check for libyaml vulnerability CVE-2014-2525 on your system") do
options[:libyaml_check] = true
end
end
option_parser.parse!
def report_libyaml_ok
puts "\e[32mGood news! You definitely have either a patched or up-to-date libyaml version :)\e[39m"
end
def check_for_overflow_bug
YAML.load("--- !#{'%20' * 100}")
report_libyaml_ok
end
def perform_libyaml_check(force=false)
unless SafeYAML::LibyamlChecker.libyaml_version_ok?
warn <<-EOM.gsub(/^ +/, ' ')
\e[33mSafeYAML Warning\e[39m
\e[33m----------------\e[39m
\e[31mYou may have an outdated version of libyaml (#{SafeYAML::LibyamlChecker::LIBYAML_VERSION}) installed on your system.\e[39m
Prior to 0.1.6, libyaml is vulnerable to a heap overflow exploit from malicious YAML payloads.
For more info, see:
https://www.ruby-lang.org/en/news/2014/03/29/heap-overflow-in-yaml-uri-escape-parsing-cve-2014-2525/
EOM
end
puts <<-EOM.gsub(/^ +/, ' ')
Hit Enter to check if your version of libyaml is vulnerable. This will run a test \e[31mwhich may crash\e[39m
\e[31mthe current process\e[39m. If it does, your system is vulnerable and you should do something about it.
Type "nm" and hit Enter if you don't want to run the check.
See the project wiki for more info:
https://github.com/dtao/safe_yaml/wiki/The-libyaml-vulnerability
EOM
if STDIN.readline.chomp("\n") != 'nm'
check_for_overflow_bug
end
end
if options[:libyaml_check]
perform_libyaml_check(options[:force_libyaml_check])
elsif options[:file]
yaml = File.read(options[:file])
result = SafeYAML.load(yaml)
puts result.inspect
else
puts option_parser.help
end
@@ -0,0 +1,11 @@
#!/bin/bash
[[ -s "$HOME/.rvm/scripts/rvm" ]] && . "$HOME/.rvm/scripts/rvm"
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")
for i in "${versions[@]}"
do
rvm use $i
bundle install
done
+94
View File
@@ -0,0 +1,94 @@
require "safe_yaml/load"
module YAML
def self.load_with_options(yaml, *original_arguments)
filename, options = filename_and_options_from_arguments(original_arguments)
safe_mode = safe_mode_from_options("load", options)
arguments = [yaml]
if safe_mode == :safe
arguments << filename if SafeYAML::YAML_ENGINE == "psych"
arguments << options_for_safe_load(options)
safe_load(*arguments)
else
arguments << filename if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
unsafe_load(*arguments)
end
end
def self.load_file_with_options(file, options={})
safe_mode = safe_mode_from_options("load_file", options)
if safe_mode == :safe
safe_load_file(file, options_for_safe_load(options))
else
unsafe_load_file(file)
end
end
def self.safe_load(*args)
SafeYAML.load(*args)
end
def self.safe_load_file(*args)
SafeYAML.load_file(*args)
end
if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
def self.unsafe_load_file(filename)
# https://github.com/tenderlove/psych/blob/v1.3.2/lib/psych.rb#L296-298
File.open(filename, 'r:bom|utf-8') { |f| self.unsafe_load(f, filename) }
end
else
def self.unsafe_load_file(filename)
# https://github.com/tenderlove/psych/blob/v1.2.2/lib/psych.rb#L231-233
self.unsafe_load File.open(filename)
end
end
class << self
alias_method :unsafe_load, :load
alias_method :load, :load_with_options
alias_method :load_file, :load_file_with_options
private
def filename_and_options_from_arguments(arguments)
if arguments.count == 1
if arguments.first.is_a?(String)
return arguments.first, {}
else
return nil, arguments.first || {}
end
else
return arguments.first, arguments.last || {}
end
end
def safe_mode_from_options(method, options={})
if options[:safe].nil?
safe_mode = SafeYAML::OPTIONS[:default_mode] || :safe
if SafeYAML::OPTIONS[:default_mode].nil? && !SafeYAML::OPTIONS[:suppress_warnings]
Kernel.warn <<-EOWARNING.gsub(/^\s+/, '')
Called '#{method}' without the :safe option -- defaulting to #{safe_mode} mode.
You can avoid this warning in the future by setting the SafeYAML::OPTIONS[:default_mode] option (to :safe or :unsafe).
EOWARNING
SafeYAML::OPTIONS[:suppress_warnings] = true
end
return safe_mode
end
options[:safe] ? :safe : :unsafe
end
def options_for_safe_load(base_options)
options = base_options.dup
options.delete(:safe)
options
end
end
end
@@ -0,0 +1,34 @@
module SafeYAML
class Deep
def self.freeze(object)
object.each do |*entry|
value = entry.last
case value
when String, Regexp
value.freeze
when Enumerable
Deep.freeze(value)
end
end
return object.freeze
end
def self.copy(object)
duplicate = object.dup rescue object
case object
when Array
(0...duplicate.count).each do |i|
duplicate[i] = Deep.copy(duplicate[i])
end
when Hash
duplicate.keys.each do |key|
duplicate[key] = Deep.copy(duplicate[key])
end
end
duplicate
end
end
end
@@ -0,0 +1,36 @@
require "set"
module SafeYAML
class LibyamlChecker
LIBYAML_VERSION = Psych::LIBYAML_VERSION rescue nil
# Do proper version comparison (e.g. so 0.1.10 is >= 0.1.6)
SAFE_LIBYAML_VERSION = Gem::Version.new("0.1.6")
KNOWN_PATCHED_LIBYAML_VERSIONS = Set.new([
# http://people.canonical.com/~ubuntu-security/cve/2014/CVE-2014-2525.html
"0.1.4-2ubuntu0.12.04.3",
"0.1.4-2ubuntu0.12.10.3",
"0.1.4-2ubuntu0.13.10.3",
"0.1.4-3ubuntu3",
# https://security-tracker.debian.org/tracker/CVE-2014-2525
"0.1.3-1+deb6u4",
"0.1.4-2+deb7u4",
"0.1.4-3.2"
]).freeze
def self.libyaml_version_ok?
return true if YAML_ENGINE != "psych" || defined?(JRUBY_VERSION)
return true if Gem::Version.new(LIBYAML_VERSION || "0") >= SAFE_LIBYAML_VERSION
return libyaml_patched?
end
def self.libyaml_patched?
return false if (`which dpkg` rescue '').empty?
libyaml_version = `dpkg -s libyaml-0-2`.match(/^Version: (.*)$/)
return false if libyaml_version.nil?
KNOWN_PATCHED_LIBYAML_VERSIONS.include?(libyaml_version[1])
end
end
end
@@ -0,0 +1,181 @@
require "set"
require "yaml"
# This needs to be defined up front in case any internal classes need to base
# their behavior off of this.
module SafeYAML
YAML_ENGINE = defined?(YAML::ENGINE) ? YAML::ENGINE.yamler : (defined?(Psych) && YAML == Psych ? "psych" : "syck")
end
require "safe_yaml/libyaml_checker"
require "safe_yaml/deep"
require "safe_yaml/parse/hexadecimal"
require "safe_yaml/parse/sexagesimal"
require "safe_yaml/parse/date"
require "safe_yaml/transform/transformation_map"
require "safe_yaml/transform/to_boolean"
require "safe_yaml/transform/to_date"
require "safe_yaml/transform/to_float"
require "safe_yaml/transform/to_integer"
require "safe_yaml/transform/to_nil"
require "safe_yaml/transform/to_symbol"
require "safe_yaml/transform"
require "safe_yaml/resolver"
require "safe_yaml/syck_hack" if SafeYAML::YAML_ENGINE == "syck" && defined?(JRUBY_VERSION)
module SafeYAML
MULTI_ARGUMENT_YAML_LOAD = YAML.method(:load).arity != 1
DEFAULT_OPTIONS = Deep.freeze({
:default_mode => nil,
:suppress_warnings => false,
:deserialize_symbols => false,
:whitelisted_tags => [],
:custom_initializers => {},
:raise_on_unknown_tag => false
})
OPTIONS = Deep.copy(DEFAULT_OPTIONS)
PREDEFINED_TAGS = {}
if YAML_ENGINE == "syck"
YAML.tagged_classes.each do |tag, klass|
PREDEFINED_TAGS[klass] = tag
end
else
# Special tags appear to be hard-coded in Psych:
# https://github.com/tenderlove/psych/blob/v1.3.4/lib/psych/visitors/to_ruby.rb
# Fortunately, there aren't many that SafeYAML doesn't already support.
PREDEFINED_TAGS.merge!({
Exception => "!ruby/exception",
Range => "!ruby/range",
Regexp => "!ruby/regexp",
})
end
Deep.freeze(PREDEFINED_TAGS)
module_function
def restore_defaults!
OPTIONS.clear.merge!(Deep.copy(DEFAULT_OPTIONS))
end
def tag_safety_check!(tag, options)
return if tag.nil? || tag == "!"
if options[:raise_on_unknown_tag] && !options[:whitelisted_tags].include?(tag) && !tag_is_explicitly_trusted?(tag)
raise "Unknown YAML tag '#{tag}'"
end
end
def whitelist!(*classes)
classes.each do |klass|
whitelist_class!(klass)
end
end
def whitelist_class!(klass)
raise "#{klass} not a Class" unless klass.is_a?(::Class)
klass_name = klass.name
raise "#{klass} cannot be anonymous" if klass_name.nil? || klass_name.empty?
# Whitelist any built-in YAML tags supplied by Syck or Psych.
predefined_tag = PREDEFINED_TAGS[klass]
if predefined_tag
OPTIONS[:whitelisted_tags] << predefined_tag
return
end
# Exception is exceptional (har har).
tag_class = klass < Exception ? "exception" : "object"
tag_prefix = case YAML_ENGINE
when "psych" then "!ruby/#{tag_class}"
when "syck" then "tag:ruby.yaml.org,2002:#{tag_class}"
else raise "unknown YAML_ENGINE #{YAML_ENGINE}"
end
OPTIONS[:whitelisted_tags] << "#{tag_prefix}:#{klass_name}"
end
if YAML_ENGINE == "psych"
def tag_is_explicitly_trusted?(tag)
false
end
else
TRUSTED_TAGS = Set.new([
"tag:yaml.org,2002:binary",
"tag:yaml.org,2002:bool#no",
"tag:yaml.org,2002:bool#yes",
"tag:yaml.org,2002:float",
"tag:yaml.org,2002:float#fix",
"tag:yaml.org,2002:int",
"tag:yaml.org,2002:map",
"tag:yaml.org,2002:null",
"tag:yaml.org,2002:seq",
"tag:yaml.org,2002:str",
"tag:yaml.org,2002:timestamp",
"tag:yaml.org,2002:timestamp#ymd"
]).freeze
def tag_is_explicitly_trusted?(tag)
TRUSTED_TAGS.include?(tag)
end
end
if SafeYAML::YAML_ENGINE == "psych"
require "safe_yaml/psych_handler"
require "safe_yaml/psych_resolver"
require "safe_yaml/safe_to_ruby_visitor"
def self.load(yaml, filename=nil, options={})
# If the user hasn't whitelisted any tags, we can go with this implementation which is
# significantly faster.
if (options && options[:whitelisted_tags] || SafeYAML::OPTIONS[:whitelisted_tags]).empty?
safe_handler = SafeYAML::PsychHandler.new(options) do |result|
return result
end
arguments_for_parse = [yaml]
arguments_for_parse << filename if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
Psych::Parser.new(safe_handler).parse(*arguments_for_parse)
return safe_handler.result
else
safe_resolver = SafeYAML::PsychResolver.new(options)
tree = SafeYAML::MULTI_ARGUMENT_YAML_LOAD ?
Psych.parse(yaml, filename) :
Psych.parse(yaml)
return safe_resolver.resolve_node(tree)
end
end
def self.load_file(filename, options={})
if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
File.open(filename, 'r:bom|utf-8') { |f| self.load(f, filename, options) }
else
# Ruby pukes on 1.9.2 if we try to open an empty file w/ 'r:bom|utf-8';
# so we'll not specify those flags here. This mirrors the behavior for
# unsafe_load_file so it's probably preferable anyway.
self.load File.open(filename), nil, options
end
end
else
require "safe_yaml/syck_resolver"
require "safe_yaml/syck_node_monkeypatch"
def self.load(yaml, options={})
resolver = SafeYAML::SyckResolver.new(SafeYAML::OPTIONS.merge(options || {}))
tree = YAML.parse(yaml)
return resolver.resolve_node(tree)
end
def self.load_file(filename, options={})
File.open(filename) { |f| self.load(f, options) }
end
end
end
@@ -0,0 +1,37 @@
require 'time'
module SafeYAML
class Parse
class Date
# This one's easy enough :)
DATE_MATCHER = /\A(\d{4})-(\d{2})-(\d{2})\Z/.freeze
# This unbelievable little gem is taken basically straight from the YAML spec, but made
# slightly more readable (to my poor eyes at least) to me:
# http://yaml.org/type/timestamp.html
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
SECONDS_PER_DAY = 60 * 60 * 24
MICROSECONDS_PER_SECOND = 1000000
# So this is weird. In Ruby 1.8.7, the DateTime#sec_fraction method returned fractional
# seconds in units of DAYS for some reason. In 1.9.2, they changed the units -- much more
# reasonably -- to seconds.
SEC_FRACTION_MULTIPLIER = RUBY_VERSION == "1.8.7" ? (SECONDS_PER_DAY * MICROSECONDS_PER_SECOND) : MICROSECONDS_PER_SECOND
# The DateTime class has a #to_time method in Ruby 1.9+;
# Before that we'll just need to convert DateTime to Time ourselves.
TO_TIME_AVAILABLE = DateTime.instance_methods.include?(:to_time)
def self.value(value)
d = DateTime.parse(value)
return d.to_time if TO_TIME_AVAILABLE
usec = d.sec_fraction * SEC_FRACTION_MULTIPLIER
time = Time.utc(d.year, d.month, d.day, d.hour, d.min, d.sec, usec) - (d.offset * SECONDS_PER_DAY)
time.getlocal
end
end
end
end
@@ -0,0 +1,12 @@
module SafeYAML
class Parse
class Hexadecimal
MATCHER = /\A[-+]?0x[0-9a-fA-F_]+\Z/.freeze
def self.value(value)
# This is safe to do since we already validated the value.
return Integer(value.gsub(/_/, ""))
end
end
end
end
@@ -0,0 +1,26 @@
module SafeYAML
class Parse
class Sexagesimal
INTEGER_MATCHER = /\A[-+]?[0-9][0-9_]*(:[0-5]?[0-9])+\Z/.freeze
FLOAT_MATCHER = /\A[-+]?[0-9][0-9_]*(:[0-5]?[0-9])+\.[0-9_]*\Z/.freeze
def self.value(value)
before_decimal, after_decimal = value.split(".")
whole_part = 0
multiplier = 1
before_decimal = before_decimal.split(":")
until before_decimal.empty?
whole_part += (Float(before_decimal.pop) * multiplier)
multiplier *= 60
end
result = whole_part
result += Float("." + after_decimal) unless after_decimal.nil?
result *= -1 if value[0] == "-"
result
end
end
end
end
@@ -0,0 +1,99 @@
require "psych"
require "base64"
module SafeYAML
class PsychHandler < Psych::Handler
def initialize(options, &block)
@options = SafeYAML::OPTIONS.merge(options || {})
@block = block
@initializers = @options[:custom_initializers] || {}
@anchors = {}
@stack = []
@current_key = nil
@result = nil
@begun = false
end
def result
@begun ? @result : false
end
def add_to_current_structure(value, anchor=nil, quoted=nil, tag=nil)
value = Transform.to_proper_type(value, quoted, tag, @options)
@anchors[anchor] = value if anchor
if !@begun
@begun = true
@result = value
@current_structure = @result
return
end
if @current_structure.respond_to?(:<<)
@current_structure << value
elsif @current_structure.respond_to?(:[]=)
if @current_key.nil?
@current_key = value
else
if @current_key == "<<"
@current_structure.merge!(value)
else
@current_structure[@current_key] = value
end
@current_key = nil
end
else
raise "Don't know how to add to a #{@current_structure.class}!"
end
end
def end_current_structure
@stack.pop
@current_structure = @stack.last
end
def streaming?
true
end
# event handlers
def alias(anchor)
add_to_current_structure(@anchors[anchor])
end
def scalar(value, anchor, tag, plain, quoted, style)
add_to_current_structure(value, anchor, quoted, tag)
end
def end_document(implicit)
@block.call(@result)
end
def start_mapping(anchor, tag, implicit, style)
map = @initializers.include?(tag) ? @initializers[tag].call : {}
self.add_to_current_structure(map, anchor)
@current_structure = map
@stack.push(map)
end
def end_mapping
self.end_current_structure()
end
def start_sequence(anchor, tag, implicit, style)
seq = @initializers.include?(tag) ? @initializers[tag].call : []
self.add_to_current_structure(seq, anchor)
@current_structure = seq
@stack.push(seq)
end
def end_sequence
self.end_current_structure()
end
end
end
@@ -0,0 +1,52 @@
module SafeYAML
class PsychResolver < Resolver
NODE_TYPES = {
Psych::Nodes::Document => :root,
Psych::Nodes::Mapping => :map,
Psych::Nodes::Sequence => :seq,
Psych::Nodes::Scalar => :scalar,
Psych::Nodes::Alias => :alias
}.freeze
def initialize(options={})
super
@aliased_nodes = {}
end
def resolve_root(root)
resolve_seq(root).first
end
def resolve_alias(node)
resolve_node(@aliased_nodes[node.anchor])
end
def native_resolve(node)
@visitor ||= SafeYAML::SafeToRubyVisitor.new(self)
@visitor.accept(node)
end
def get_node_type(node)
NODE_TYPES[node.class]
end
def get_node_tag(node)
node.tag
end
def get_node_value(node)
@aliased_nodes[node.anchor] = node if node.respond_to?(:anchor) && node.anchor
case get_node_type(node)
when :root, :map, :seq
node.children
when :scalar
node.value
end
end
def value_is_quoted?(node)
node.quoted
end
end
end
@@ -0,0 +1,94 @@
module SafeYAML
class Resolver
def initialize(options)
@options = SafeYAML::OPTIONS.merge(options || {})
@whitelist = @options[:whitelisted_tags] || []
@initializers = @options[:custom_initializers] || {}
@raise_on_unknown_tag = @options[:raise_on_unknown_tag]
end
def resolve_node(node)
return node if !node
return self.native_resolve(node) if tag_is_whitelisted?(self.get_node_tag(node))
case self.get_node_type(node)
when :root
resolve_root(node)
when :map
resolve_map(node)
when :seq
resolve_seq(node)
when :scalar
resolve_scalar(node)
when :alias
resolve_alias(node)
else
raise "Don't know how to resolve this node: #{node.inspect}"
end
end
def resolve_map(node)
tag = get_and_check_node_tag(node)
hash = @initializers.include?(tag) ? @initializers[tag].call : {}
map = normalize_map(self.get_node_value(node))
# Take the "<<" key nodes first, as these are meant to approximate a form of inheritance.
inheritors = map.select { |key_node, value_node| resolve_node(key_node) == "<<" }
inheritors.each do |key_node, value_node|
merge_into_hash(hash, resolve_node(value_node))
end
# All that's left should be normal (non-"<<") nodes.
(map - inheritors).each do |key_node, value_node|
hash[resolve_node(key_node)] = resolve_node(value_node)
end
return hash
end
def resolve_seq(node)
seq = self.get_node_value(node)
tag = get_and_check_node_tag(node)
arr = @initializers.include?(tag) ? @initializers[tag].call : []
seq.inject(arr) { |array, n| array << resolve_node(n) }
end
def resolve_scalar(node)
Transform.to_proper_type(self.get_node_value(node), self.value_is_quoted?(node), get_and_check_node_tag(node), @options)
end
def get_and_check_node_tag(node)
tag = self.get_node_tag(node)
SafeYAML.tag_safety_check!(tag, @options)
tag
end
def tag_is_whitelisted?(tag)
@whitelist.include?(tag)
end
def options
@options
end
private
def normalize_map(map)
# Syck creates Hashes from maps.
if map.is_a?(Hash)
map.inject([]) { |arr, key_and_value| arr << key_and_value }
# Psych is really weird; it flattens out a Hash completely into: [key, value, key, value, ...]
else
map.each_slice(2).to_a
end
end
def merge_into_hash(hash, array)
array.each do |key, value|
hash[key] = value
end
end
end
end
@@ -0,0 +1,29 @@
module SafeYAML
class SafeToRubyVisitor < Psych::Visitors::ToRuby
INITIALIZE_ARITY = superclass.instance_method(:initialize).arity
def initialize(resolver)
case INITIALIZE_ARITY
when 2
# https://github.com/tenderlove/psych/blob/v2.0.0/lib/psych/visitors/to_ruby.rb#L14-L28
loader = Psych::ClassLoader.new
scanner = Psych::ScalarScanner.new(loader)
super(scanner, loader)
else
super()
end
@resolver = resolver
end
def accept(node)
if node.tag
SafeYAML.tag_safety_check!(node.tag, @resolver.options)
return super
end
@resolver.resolve_node(node)
end
end
end
@@ -0,0 +1,39 @@
require 'safe_yaml/load'
require 'yaml/store'
module SafeYAML
class Store < YAML::Store
# Override YAML::Store#initialize to accept additional option
# +safe_yaml_opts+.
def initialize(file_name, yaml_opts = {}, safe_yaml_opts = {})
@safe_yaml_opts = safe_yaml_opts
super(file_name, yaml_opts)
end
# Override YAML::Store#load to use SafeYAML.load instead of
# YAML.load (via #safe_yaml_load).
#--
# PStore#load is private, while YAML::Store#load is public.
#++
def load(content)
table = safe_yaml_load(content)
table == false ? {} : table
end
private
if SafeYAML::YAML_ENGINE == 'psych'
def safe_yaml_load(content)
SafeYAML.load(content, nil, @safe_yaml_opts)
end
else
def safe_yaml_load(content)
SafeYAML.load(content, @safe_yaml_opts)
end
end
end
end
@@ -0,0 +1,36 @@
# Hack to JRuby 1.8's YAML Parser Yecht
#
# This file is always loaded AFTER either syck or psych are already
# loaded. It then looks at what constants are available and creates
# a consistent view on all rubys.
#
# Taken from rubygems and modified.
# See https://github.com/rubygems/rubygems/blob/master/lib/rubygems/syck_hack.rb
module YAML
# In newer 1.9.2, there is a Syck toplevel constant instead of it
# being underneith YAML. If so, reference it back under YAML as
# well.
if defined? ::Syck
# for tests that change YAML::ENGINE
# 1.8 does not support the second argument to const_defined?
remove_const :Syck rescue nil
Syck = ::Syck
# JRuby's "Syck" is called "Yecht"
elsif defined? YAML::Yecht
Syck = YAML::Yecht
end
end
# Sometime in the 1.9 dev cycle, the Syck constant was moved from under YAML
# to be a toplevel constant. So gemspecs created under these versions of Syck
# will have references to Syck::DefaultKey.
#
# So we need to be sure that we reference Syck at the toplevel too so that
# we can always load these kind of gemspecs.
#
if !defined?(Syck)
Syck = YAML::Syck
end
@@ -0,0 +1,43 @@
# This is, admittedly, pretty insane. Fundamentally the challenge here is this: if we want to allow
# whitelisting of tags (while still leveraging Syck's internal functionality), then we have to
# change how Syck::Node#transform works. But since we (SafeYAML) do not control instantiation of
# Syck::Node objects, we cannot, for example, subclass Syck::Node and override #tranform the "easy"
# way. So the only choice is to monkeypatch, like this. And the only way to make this work
# recursively with potentially call-specific options (that my feeble brain can think of) is to set
# pseudo-global options on the first call and unset them once the recursive stack has fully unwound.
monkeypatch = <<-EORUBY
class Node
@@safe_transform_depth = 0
@@safe_transform_whitelist = nil
def safe_transform(options={})
begin
@@safe_transform_depth += 1
@@safe_transform_whitelist ||= options[:whitelisted_tags]
if self.type_id
SafeYAML.tag_safety_check!(self.type_id, options)
return unsafe_transform if @@safe_transform_whitelist.include?(self.type_id)
end
SafeYAML::SyckResolver.new.resolve_node(self)
ensure
@@safe_transform_depth -= 1
if @@safe_transform_depth == 0
@@safe_transform_whitelist = nil
end
end
end
alias_method :unsafe_transform, :transform
alias_method :transform, :safe_transform
end
EORUBY
if defined?(YAML::Syck::Node)
YAML::Syck.module_eval monkeypatch
else
Syck.module_eval monkeypatch
end
@@ -0,0 +1,38 @@
module SafeYAML
class SyckResolver < Resolver
QUOTE_STYLES = [
:quote1,
:quote2
].freeze
NODE_TYPES = {
Hash => :map,
Array => :seq,
String => :scalar
}.freeze
def initialize(options={})
super
end
def native_resolve(node)
node.transform(self.options)
end
def get_node_type(node)
NODE_TYPES[node.value.class]
end
def get_node_tag(node)
node.type_id
end
def get_node_value(node)
node.value
end
def value_is_quoted?(node)
QUOTE_STYLES.include?(node.instance_variable_get(:@style))
end
end
end
@@ -0,0 +1,41 @@
require 'base64'
module SafeYAML
class Transform
TRANSFORMERS = [
Transform::ToSymbol.new,
Transform::ToInteger.new,
Transform::ToFloat.new,
Transform::ToNil.new,
Transform::ToBoolean.new,
Transform::ToDate.new
]
def self.to_guessed_type(value, quoted=false, options=nil)
return value if quoted
if value.is_a?(String)
TRANSFORMERS.each do |transformer|
success, transformed_value = transformer.method(:transform?).arity == 1 ?
transformer.transform?(value) :
transformer.transform?(value, options)
return transformed_value if success
end
end
value
end
def self.to_proper_type(value, quoted=false, tag=nil, options=nil)
case tag
when "tag:yaml.org,2002:binary", "x-private:binary", "!binary"
decoded = Base64.decode64(value)
decoded = decoded.force_encoding(value.encoding) if decoded.respond_to?(:force_encoding)
decoded
else
self.to_guessed_type(value, quoted, options)
end
end
end
end
@@ -0,0 +1,21 @@
module SafeYAML
class Transform
class ToBoolean
include TransformationMap
set_predefined_values({
"yes" => true,
"on" => true,
"true" => true,
"no" => false,
"off" => false,
"false" => false
})
def transform?(value)
return false if value.length > 5
return PREDEFINED_VALUES.include?(value), PREDEFINED_VALUES[value]
end
end
end
end
@@ -0,0 +1,13 @@
module SafeYAML
class Transform
class ToDate
def transform?(value)
return true, Date.parse(value) if Parse::Date::DATE_MATCHER.match(value)
return true, Parse::Date.value(value) if Parse::Date::TIME_MATCHER.match(value)
false
rescue ArgumentError
return true, value
end
end
end
end
@@ -0,0 +1,33 @@
module SafeYAML
class Transform
class ToFloat
Infinity = 1.0 / 0.0
NaN = 0.0 / 0.0
PREDEFINED_VALUES = {
".inf" => Infinity,
".Inf" => Infinity,
".INF" => Infinity,
"-.inf" => -Infinity,
"-.Inf" => -Infinity,
"-.INF" => -Infinity,
".nan" => NaN,
".NaN" => NaN,
".NAN" => NaN,
}.freeze
MATCHER = /\A[-+]?(?:\d[\d_]*)?\.[\d_]+(?:[eE][-+][\d]+)?\Z/.freeze
def transform?(value)
return true, Float(value) if MATCHER.match(value)
try_edge_cases?(value)
end
def try_edge_cases?(value)
return true, PREDEFINED_VALUES[value] if PREDEFINED_VALUES.include?(value)
return true, Parse::Sexagesimal.value(value) if Parse::Sexagesimal::FLOAT_MATCHER.match(value)
return false
end
end
end
end
@@ -0,0 +1,26 @@
module SafeYAML
class Transform
class ToInteger
MATCHERS = Deep.freeze([
/\A[-+]?(0|([1-9][0-9_,]*))\Z/, # decimal
/\A0[0-7]+\Z/, # octal
/\A0x[0-9a-f]+\Z/i, # hexadecimal
/\A0b[01_]+\Z/ # binary
])
def transform?(value)
MATCHERS.each_with_index do |matcher, idx|
value = value.gsub(/[_,]/, "") if idx == 0
return true, Integer(value) if matcher.match(value)
end
try_edge_cases?(value)
end
def try_edge_cases?(value)
return true, Parse::Hexadecimal.value(value) if Parse::Hexadecimal::MATCHER.match(value)
return true, Parse::Sexagesimal.value(value) if Parse::Sexagesimal::INTEGER_MATCHER.match(value)
return false
end
end
end
end
@@ -0,0 +1,18 @@
module SafeYAML
class Transform
class ToNil
include TransformationMap
set_predefined_values({
"" => nil,
"~" => nil,
"null" => nil
})
def transform?(value)
return false if value.length > 4
return PREDEFINED_VALUES.include?(value), PREDEFINED_VALUES[value]
end
end
end
end
@@ -0,0 +1,17 @@
module SafeYAML
class Transform
class ToSymbol
def transform?(value, options=SafeYAML::OPTIONS)
if options[:deserialize_symbols] && value =~ /\A:./
if value =~ /\A:(["'])(.*)\1\Z/
return true, $2.sub(/^:/, "").to_sym
else
return true, value.sub(/^:/, "").to_sym
end
end
return false
end
end
end
end
@@ -0,0 +1,47 @@
module SafeYAML
class Transform
module TransformationMap
def self.included(base)
base.extend(ClassMethods)
end
class CaseAgnosticMap < Hash
def initialize(*args)
super
end
def include?(key)
super(key.downcase)
end
def [](key)
super(key.downcase)
end
# OK, I actually don't think it's all that important that this map be
# frozen.
def freeze
self
end
end
module ClassMethods
def set_predefined_values(predefined_values)
if SafeYAML::YAML_ENGINE == "syck"
expanded_map = predefined_values.inject({}) do |hash, (key, value)|
hash[key] = value
hash[key.capitalize] = value
hash[key.upcase] = value
hash
end
else
expanded_map = CaseAgnosticMap.new
expanded_map.merge!(predefined_values)
end
self.const_set(:PREDEFINED_VALUES, expanded_map.freeze)
end
end
end
end
end
@@ -0,0 +1,3 @@
module SafeYAML
VERSION = "1.0.5"
end
+38
View File
@@ -0,0 +1,38 @@
#!/bin/bash
[[ -s "$HOME/.rvm/scripts/rvm" ]] && . "$HOME/.rvm/scripts/rvm"
rvm use 1.8.7
bundle exec rake spec
rvm use 1.9.2
YAMLER=syck bundle exec rake spec
YAMLER=psych bundle exec rake spec
rvm use 1.9.3
YAMLER=syck bundle exec rake spec
YAMLER=psych bundle exec rake spec
rvm use 2.0.0
bundle exec rake spec
rvm use 2.1.0
bundle exec rake spec
rvm use 2.1.1
bundle exec rake spec
rvm use 2.1.2
bundle exec rake spec
rvm use ruby-head
bundle exec rake spec
rvm use jruby
JRUBY_OPTS=--1.8 bundle exec rake spec
JRUBY_OPTS=--1.9 bundle exec rake spec
JRUBY_OPTS=--2.0 bundle exec rake spec
@@ -0,0 +1,19 @@
# -*- encoding: utf-8 -*-
require File.join(File.dirname(__FILE__), "lib", "safe_yaml", "version")
Gem::Specification.new do |gem|
gem.name = "safe_yaml"
gem.version = SafeYAML::VERSION
gem.authors = "Dan Tao"
gem.email = "daniel.tao@gmail.com"
gem.description = %q{Parse YAML safely}
gem.summary = %q{SameYAML provides an alternative implementation of YAML.load suitable for accepting user input in Ruby applications.}
gem.homepage = "https://github.com/dtao/safe_yaml"
gem.license = "MIT"
gem.files = `git ls-files`.split($\)
gem.test_files = gem.files.grep(%r{^spec/})
gem.require_paths = ["lib"]
gem.executables = ["safe_yaml"]
gem.required_ruby_version = ">= 1.8.7"
end
@@ -0,0 +1,2 @@
--- !ruby/object:ExploitableBackDoor
foo: bar
@@ -0,0 +1,2 @@
--- !ruby/hash:ExploitableBackDoor
foo: bar
+20
View File
@@ -0,0 +1,20 @@
---
title: Blah
key: value
---
I'm going to inject a bunch of YAML-looking stuff below and it should all just get ignored.
foo: bar
- foo
- bar
:foo
42
~
---
text: |
Look, I'm another YAML document!
---
@@ -0,0 +1,69 @@
require "spec_helper"
describe SafeYAML::LibyamlChecker do
describe "check_libyaml_version" do
REAL_YAML_ENGINE = SafeYAML::YAML_ENGINE
REAL_LIBYAML_VERSION = SafeYAML::LibyamlChecker::LIBYAML_VERSION
let(:libyaml_patched) { false }
before :each do
allow(SafeYAML::LibyamlChecker).to receive(:libyaml_patched?).and_return(libyaml_patched)
end
after :each do
silence_warnings do
SafeYAML::YAML_ENGINE = REAL_YAML_ENGINE
SafeYAML::LibyamlChecker::LIBYAML_VERSION = REAL_LIBYAML_VERSION
end
end
def test_libyaml_version_ok(expected_result, yaml_engine, libyaml_version=nil)
silence_warnings do
SafeYAML.const_set("YAML_ENGINE", yaml_engine)
SafeYAML::LibyamlChecker.const_set("LIBYAML_VERSION", libyaml_version)
expect(SafeYAML::LibyamlChecker.libyaml_version_ok?).to eq(expected_result)
end
end
unless defined?(JRUBY_VERSION)
it "issues no warnings when 'Syck' is the YAML engine" do
test_libyaml_version_ok(true, "syck")
end
it "issues a warning if Psych::LIBYAML_VERSION is not defined" do
test_libyaml_version_ok(false, "psych")
end
it "issues a warning if Psych::LIBYAML_VERSION is < 0.1.6" do
test_libyaml_version_ok(false, "psych", "0.1.5")
end
it "issues no warning if Psych::LIBYAML_VERSION is == 0.1.6" do
test_libyaml_version_ok(true, "psych", "0.1.6")
end
it "issues no warning if Psych::LIBYAML_VERSION is > 0.1.6" do
test_libyaml_version_ok(true, "psych", "1.0.0")
end
it "does a proper version comparison (not just a string comparison)" do
test_libyaml_version_ok(true, "psych", "0.1.10")
end
context "when the system has a known patched libyaml version" do
let(:libyaml_patched) { true }
it "issues no warning, even when Psych::LIBYAML_VERSION < 0.1.6" do
test_libyaml_version_ok(true, "psych", "0.1.4")
end
end
end
if defined?(JRUBY_VERSION)
it "issues no warning, as JRuby doesn't use libyaml" do
test_libyaml_version_ok(true, "psych", "0.1.4")
end
end
end
end
@@ -0,0 +1,10 @@
require "spec_helper"
if SafeYAML::YAML_ENGINE == "psych"
require "safe_yaml/psych_resolver"
describe SafeYAML::PsychResolver do
include ResolverSpecs
let(:resolver) { SafeYAML::PsychResolver.new }
end
end
@@ -0,0 +1,278 @@
module ResolverSpecs
def self.included(base)
base.module_eval do
let(:resolver) { nil }
let(:result) { @result }
before :each do
# See the comment in the first before :each block in safe_yaml_spec.rb.
require "safe_yaml"
end
def parse(yaml)
tree = YAML.parse(yaml.unindent)
@result = resolver.resolve_node(tree)
end
# Isn't this how I should've been doing it all along?
def parse_and_test(yaml)
safe_result = parse(yaml)
exception_thrown = nil
unsafe_result = begin
YAML.unsafe_load(yaml)
rescue Exception => e
exception_thrown = e
end
if exception_thrown
# If the underlying YAML parser (e.g. Psych) threw an exception, I'm
# honestly not sure what the right thing to do is. For now I'll just
# print a warning. Should SafeYAML fail when Psych fails?
Kernel.warn "\n"
Kernel.warn "Discrepancy between SafeYAML and #{SafeYAML::YAML_ENGINE} on input:\n"
Kernel.warn "#{yaml.unindent}\n"
Kernel.warn "SafeYAML result:"
Kernel.warn "#{safe_result.inspect}\n"
Kernel.warn "#{SafeYAML::YAML_ENGINE} result:"
Kernel.warn "#{exception_thrown.inspect}\n"
else
expect(safe_result).to eq(unsafe_result)
end
end
context "by default" do
it "translates maps to hashes" do
parse <<-YAML
potayto: potahto
tomayto: tomahto
YAML
expect(result).to eq({
"potayto" => "potahto",
"tomayto" => "tomahto"
})
end
it "translates sequences to arrays" do
parse <<-YAML
- foo
- bar
- baz
YAML
expect(result).to eq(["foo", "bar", "baz"])
end
it "translates most values to strings" do
parse "string: value"
expect(result).to eq({ "string" => "value" })
end
it "does not deserialize symbols" do
parse ":symbol: value"
expect(result).to eq({ ":symbol" => "value" })
end
it "translates valid integral numbers to integers" do
parse "integer: 1"
expect(result).to eq({ "integer" => 1 })
end
it "translates valid decimal numbers to floats" do
parse "float: 3.14"
expect(result).to eq({ "float" => 3.14 })
end
it "translates valid dates" do
parse "date: 2013-01-24"
expect(result).to eq({ "date" => Date.parse("2013-01-24") })
end
it "translates valid true/false values to booleans" do
parse <<-YAML
- yes
- true
- no
- false
YAML
expect(result).to eq([true, true, false, false])
end
it "translates valid nulls to nil" do
parse <<-YAML
-
- ~
- null
YAML
expect(result).to eq([nil] * 3)
end
it "matches the behavior of the underlying YAML engine w/ respect to capitalization of boolean values" do
parse_and_test <<-YAML
- true
- True
- TRUE
- tRue
- TRue
- False
- FALSE
- fAlse
- FALse
YAML
# using Syck: [true, true, true, "tRue", "TRue", false, false, "fAlse", "FALse"]
# using Psych: all booleans
end
it "matches the behavior of the underlying YAML engine w/ respect to capitalization of nil values" do
parse_and_test <<-YAML
- Null
- NULL
- nUll
- NUll
YAML
# using Syck: [nil, nil, "nUll", "NUll"]
# using Psych: all nils
end
it "translates quoted empty strings to strings (not nil)" do
parse "foo: ''"
expect(result).to eq({ "foo" => "" })
end
it "correctly reverse-translates strings encoded via #to_yaml" do
parse "5.10".to_yaml
expect(result).to eq("5.10")
end
it "does not specially parse any double-quoted strings" do
parse <<-YAML
- "1"
- "3.14"
- "true"
- "false"
- "2013-02-03"
- "2013-02-03 16:27:00 -0600"
YAML
expect(result).to eq(["1", "3.14", "true", "false", "2013-02-03", "2013-02-03 16:27:00 -0600"])
end
it "does not specially parse any single-quoted strings" do
parse <<-YAML
- '1'
- '3.14'
- 'true'
- 'false'
- '2013-02-03'
- '2013-02-03 16:27:00 -0600'
YAML
expect(result).to eq(["1", "3.14", "true", "false", "2013-02-03", "2013-02-03 16:27:00 -0600"])
end
it "deals just fine with nested maps" do
parse <<-YAML
foo:
bar:
marco: polo
YAML
expect(result).to eq({ "foo" => { "bar" => { "marco" => "polo" } } })
end
it "deals just fine with nested sequences" do
parse <<-YAML
- foo
-
- bar1
- bar2
-
- baz1
- baz2
YAML
expect(result).to eq(["foo", ["bar1", "bar2", ["baz1", "baz2"]]])
end
it "applies the same transformations to keys as to values" do
parse <<-YAML
foo: string
:bar: symbol
1: integer
3.14: float
2013-01-24: date
YAML
expect(result).to eq({
"foo" => "string",
":bar" => "symbol",
1 => "integer",
3.14 => "float",
Date.parse("2013-01-24") => "date",
})
end
it "applies the same transformations to elements in sequences as to all values" do
parse <<-YAML
- foo
- :bar
- 1
- 3.14
- 2013-01-24
YAML
expect(result).to eq(["foo", ":bar", 1, 3.14, Date.parse("2013-01-24")])
end
end
context "for Ruby version #{RUBY_VERSION}" do
it "translates valid time values" do
parse "time: 2013-01-29 05:58:00 -0800"
expect(result).to eq({ "time" => Time.utc(2013, 1, 29, 13, 58, 0) })
end
it "applies the same transformation to elements in sequences" do
parse "- 2013-01-29 05:58:00 -0800"
expect(result).to eq([Time.utc(2013, 1, 29, 13, 58, 0)])
end
it "applies the same transformation to keys" do
parse "2013-01-29 05:58:00 -0800: time"
expect(result).to eq({ Time.utc(2013, 1, 29, 13, 58, 0) => "time" })
end
end
context "with symbol deserialization enabled" do
before :each do
SafeYAML::OPTIONS[:deserialize_symbols] = true
end
after :each do
SafeYAML.restore_defaults!
end
it "translates values starting with ':' to symbols" do
parse "symbol: :value"
expect(result).to eq({ "symbol" => :value })
end
it "applies the same transformation to keys" do
parse ":bar: symbol"
expect(result).to eq({ :bar => "symbol" })
end
it "applies the same transformation to elements in sequences" do
parse "- :bar"
expect(result).to eq([:bar])
end
end
end
end
end
@@ -0,0 +1,731 @@
require "spec_helper"
describe YAML do
def safe_load_round_trip(object, options={})
yaml = object.to_yaml
if SafeYAML::YAML_ENGINE == "psych"
YAML.safe_load(yaml, nil, options)
else
YAML.safe_load(yaml, options)
end
end
before :each do
# Need to require this here (as opposed to somewhere up higher in the file)
# to ensure that safe_yaml isn't loaded and therefore YAML isn't monkey-
# patched, for tests that require only safe_yaml/load.
require "safe_yaml"
require "exploitable_back_door"
SafeYAML.restore_defaults!
end
after :each do
SafeYAML.restore_defaults!
end
describe "unsafe_load" do
if SafeYAML::YAML_ENGINE == "psych" && RUBY_VERSION >= "1.9.3"
it "allows exploits through objects defined in YAML w/ !ruby/hash via custom :[]= methods" do
backdoor = YAML.unsafe_load("--- !ruby/hash:ExploitableBackDoor\nfoo: bar\n")
expect(backdoor).to be_exploited_through_setter
end
it "allows exploits through objects defined in YAML w/ !ruby/object via the :init_with method" do
backdoor = YAML.unsafe_load("--- !ruby/object:ExploitableBackDoor\nfoo: bar\n")
expect(backdoor).to be_exploited_through_init_with
end
end
it "allows exploits through objects w/ sensitive instance variables defined in YAML w/ !ruby/object" do
backdoor = YAML.unsafe_load("--- !ruby/object:ExploitableBackDoor\nfoo: bar\n")
expect(backdoor).to be_exploited_through_ivars
end
context "with special whitelisted tags defined" do
before :each do
SafeYAML::whitelist!(OpenStruct)
end
it "effectively ignores the whitelist (since everything is whitelisted)" do
result = YAML.unsafe_load <<-YAML.unindent
--- !ruby/object:OpenStruct
table:
:backdoor: !ruby/object:ExploitableBackDoor
foo: bar
YAML
expect(result).to be_a(OpenStruct)
expect(result.backdoor).to be_exploited_through_ivars
end
end
end
describe "safe_load" do
it "does NOT allow exploits through objects defined in YAML w/ !ruby/hash" do
object = YAML.safe_load("--- !ruby/hash:ExploitableBackDoor\nfoo: bar\n")
expect(object).not_to be_a(ExploitableBackDoor)
end
it "does NOT allow exploits through objects defined in YAML w/ !ruby/object" do
object = YAML.safe_load("--- !ruby/object:ExploitableBackDoor\nfoo: bar\n")
expect(object).not_to be_a(ExploitableBackDoor)
end
context "for YAML engine #{SafeYAML::YAML_ENGINE}" do
if SafeYAML::YAML_ENGINE == "psych"
let(:options) { nil }
let(:arguments) { ["foo: bar", nil, options] }
context "when no tags are whitelisted" do
it "constructs a SafeYAML::PsychHandler to resolve nodes as they're parsed, for optimal performance" do
expect(Psych::Parser).to receive(:new).with an_instance_of(SafeYAML::PsychHandler)
# This won't work now; we just want to ensure Psych::Parser#parse was in fact called.
YAML.safe_load(*arguments) rescue nil
end
end
context "when whitelisted tags are specified" do
let(:options) {
{ :whitelisted_tags => ["foo"] }
}
it "instead uses Psych to construct a full tree before examining the nodes" do
expect(Psych).to receive(:parse)
# This won't work now; we just want to ensure Psych::Parser#parse was in fact called.
YAML.safe_load(*arguments) rescue nil
end
end
end
if SafeYAML::YAML_ENGINE == "syck"
it "uses Syck internally to parse YAML" do
expect(YAML).to receive(:parse).with("foo: bar")
# This won't work now; we just want to ensure YAML::parse was in fact called.
YAML.safe_load("foo: bar") rescue nil
end
end
end
it "loads a plain ol' YAML document just fine" do
result = YAML.safe_load <<-YAML.unindent
foo:
number: 1
boolean: true
nil: ~
string: Hello, there!
symbol: :blah
sequence:
- hi
- bye
YAML
expect(result).to eq({
"foo" => {
"number" => 1,
"boolean" => true,
"nil" => nil,
"string" => "Hello, there!",
"symbol" => ":blah",
"sequence" => ["hi", "bye"]
}
})
end
it "works for YAML documents with anchors and aliases" do
result = YAML.safe_load <<-YAML
- &id001 {}
- *id001
- *id001
YAML
expect(result).to eq([{}, {}, {}])
end
it "works for YAML documents with binary tagged keys" do
result = YAML.safe_load <<-YAML
? !!binary >
Zm9v
: "bar"
? !!binary >
YmFy
: "baz"
YAML
expect(result).to eq({"foo" => "bar", "bar" => "baz"})
end
it "works for YAML documents with binary tagged values" do
result = YAML.safe_load <<-YAML
"foo": !!binary >
YmFy
"bar": !!binary >
YmF6
YAML
expect(result).to eq({"foo" => "bar", "bar" => "baz"})
end
it "works for YAML documents with binary tagged array values" do
result = YAML.safe_load <<-YAML
- !binary |-
Zm9v
- !binary |-
YmFy
YAML
expect(result).to eq(["foo", "bar"])
end
it "works for YAML documents with sections" do
result = YAML.safe_load <<-YAML
mysql: &mysql
adapter: mysql
pool: 30
login: &login
username: user
password: password123
development: &development
<<: *mysql
<<: *login
host: localhost
YAML
expect(result).to eq({
"mysql" => {
"adapter" => "mysql",
"pool" => 30
},
"login" => {
"username" => "user",
"password" => "password123"
},
"development" => {
"adapter" => "mysql",
"pool" => 30,
"username" => "user",
"password" => "password123",
"host" => "localhost"
}
})
end
it "correctly prefers explicitly defined values over default values from included sections" do
# Repeating this test 100 times to increase the likelihood of running into an issue caused by
# non-deterministic hash key enumeration.
100.times do
result = YAML.safe_load <<-YAML
defaults: &defaults
foo: foo
bar: bar
baz: baz
custom:
<<: *defaults
bar: custom_bar
baz: custom_baz
YAML
expect(result["custom"]).to eq({
"foo" => "foo",
"bar" => "custom_bar",
"baz" => "custom_baz"
})
end
end
it "works with multi-level inheritance" do
result = YAML.safe_load <<-YAML
defaults: &defaults
foo: foo
bar: bar
baz: baz
custom: &custom
<<: *defaults
bar: custom_bar
baz: custom_baz
grandcustom: &grandcustom
<<: *custom
YAML
expect(result).to eq({
"defaults" => { "foo" => "foo", "bar" => "bar", "baz" => "baz" },
"custom" => { "foo" => "foo", "bar" => "custom_bar", "baz" => "custom_baz" },
"grandcustom" => { "foo" => "foo", "bar" => "custom_bar", "baz" => "custom_baz" }
})
end
it "returns false when parsing an empty document" do
expect([
YAML.safe_load(""),
YAML.safe_load(" "),
YAML.safe_load("\n")
]).to eq([false, false, false])
end
it "returns nil when parsing a single value representing nil" do
expect([
YAML.safe_load("~"),
YAML.safe_load("null")
]).to eq([nil, nil])
end
context "with custom initializers defined" do
before :each do
if SafeYAML::YAML_ENGINE == "psych"
SafeYAML::OPTIONS[:custom_initializers] = {
"!set" => lambda { Set.new },
"!hashiemash" => lambda { Hashie::Mash.new }
}
else
SafeYAML::OPTIONS[:custom_initializers] = {
"tag:yaml.org,2002:set" => lambda { Set.new },
"tag:yaml.org,2002:hashiemash" => lambda { Hashie::Mash.new }
}
end
end
it "will use a custom initializer to instantiate an array-like class upon deserialization" do
result = YAML.safe_load <<-YAML.unindent
--- !set
- 1
- 2
- 3
YAML
expect(result).to be_a(Set)
expect(result.to_a).to match_array([1, 2, 3])
end
it "will use a custom initializer to instantiate a hash-like class upon deserialization" do
result = YAML.safe_load <<-YAML.unindent
--- !hashiemash
foo: bar
YAML
expect(result).to be_a(Hashie::Mash)
expect(result.to_hash).to eq({ "foo" => "bar" })
end
end
context "with special whitelisted tags defined" do
before :each do
SafeYAML::whitelist!(OpenStruct)
# Necessary for deserializing OpenStructs properly.
SafeYAML::OPTIONS[:deserialize_symbols] = true
end
it "will allow objects to be deserialized for whitelisted tags" do
result = YAML.safe_load("--- !ruby/object:OpenStruct\ntable:\n foo: bar\n")
expect(result).to be_a(OpenStruct)
expect(result.instance_variable_get(:@table)).to eq({ "foo" => "bar" })
end
it "will not deserialize objects without whitelisted tags" do
result = YAML.safe_load("--- !ruby/hash:ExploitableBackDoor\nfoo: bar\n")
expect(result).not_to be_a(ExploitableBackDoor)
expect(result).to eq({ "foo" => "bar" })
end
it "will not allow non-whitelisted objects to be embedded within objects with whitelisted tags" do
result = YAML.safe_load <<-YAML.unindent
--- !ruby/object:OpenStruct
table:
:backdoor: !ruby/object:ExploitableBackDoor
foo: bar
YAML
expect(result).to be_a(OpenStruct)
expect(result.backdoor).not_to be_a(ExploitableBackDoor)
expect(result.backdoor).to eq({ "foo" => "bar" })
end
context "with the :raise_on_unknown_tag option enabled" do
before :each do
SafeYAML::OPTIONS[:raise_on_unknown_tag] = true
end
after :each do
SafeYAML.restore_defaults!
end
it "raises an exception if a non-nil, non-whitelisted tag is encountered" do
expect {
YAML.safe_load <<-YAML.unindent
--- !ruby/object:Unknown
foo: bar
YAML
}.to raise_error
end
it "checks all tags, even those within objects with trusted tags" do
expect {
YAML.safe_load <<-YAML.unindent
--- !ruby/object:OpenStruct
table:
:backdoor: !ruby/object:Unknown
foo: bar
YAML
}.to raise_error
end
it "does not raise an exception as long as all tags are whitelisted" do
result = YAML.safe_load <<-YAML.unindent
--- !ruby/object:OpenStruct
table:
:backdoor:
string: foo
integer: 1
float: 3.14
symbol: :bar
date: 2013-02-20
array: []
hash: {}
YAML
expect(result).to be_a(OpenStruct)
expect(result.backdoor).to eq({
"string" => "foo",
"integer" => 1,
"float" => 3.14,
"symbol" => :bar,
"date" => Date.parse("2013-02-20"),
"array" => [],
"hash" => {}
})
end
it "does not raise an exception on the non-specific '!' tag" do
result = nil
expect { result = YAML.safe_load "--- ! 'foo'" }.to_not raise_error
expect(result).to eq("foo")
end
context "with whitelisted custom class" do
class SomeClass
attr_accessor :foo
end
let(:instance) { SomeClass.new }
before do
SafeYAML::whitelist!(SomeClass)
instance.foo = 'with trailing whitespace: '
end
it "does not raise an exception on the non-specific '!' tag" do
result = nil
expect { result = YAML.safe_load(instance.to_yaml) }.to_not raise_error
expect(result.foo).to eq('with trailing whitespace: ')
end
end
end
end
context "when options are passed direclty to #load which differ from the defaults" do
let(:default_options) { {} }
before :each do
SafeYAML::OPTIONS.merge!(default_options)
end
context "(for example, when symbol deserialization is enabled by default)" do
let(:default_options) { { :deserialize_symbols => true } }
it "goes with the default option when it is not overridden" do
silence_warnings do
expect(YAML.load(":foo: bar")).to eq({ :foo => "bar" })
end
end
it "allows the default option to be overridden on a per-call basis" do
silence_warnings do
expect(YAML.load(":foo: bar", :deserialize_symbols => false)).to eq({ ":foo" => "bar" })
expect(YAML.load(":foo: bar", :deserialize_symbols => true)).to eq({ :foo => "bar" })
end
end
end
context "(or, for example, when certain tags are whitelisted)" do
let(:default_options) {
{
:deserialize_symbols => true,
:whitelisted_tags => SafeYAML::YAML_ENGINE == "psych" ?
["!ruby/object:OpenStruct"] :
["tag:ruby.yaml.org,2002:object:OpenStruct"]
}
}
it "goes with the default option when it is not overridden" do
result = safe_load_round_trip(OpenStruct.new(:foo => "bar"))
expect(result).to be_a(OpenStruct)
expect(result.foo).to eq("bar")
end
it "allows the default option to be overridden on a per-call basis" do
result = safe_load_round_trip(OpenStruct.new(:foo => "bar"), :whitelisted_tags => [])
expect(result).to eq({ "table" => { :foo => "bar" } })
result = safe_load_round_trip(OpenStruct.new(:foo => "bar"), :deserialize_symbols => false, :whitelisted_tags => [])
expect(result).to eq({ "table" => { ":foo" => "bar" } })
end
end
end
end
describe "unsafe_load_file" do
if SafeYAML::YAML_ENGINE == "psych" && RUBY_VERSION >= "1.9.3"
it "allows exploits through objects defined in YAML w/ !ruby/hash via custom :[]= methods" do
backdoor = YAML.unsafe_load_file "spec/exploit.1.9.3.yaml"
expect(backdoor).to be_exploited_through_setter
end
end
if SafeYAML::YAML_ENGINE == "psych" && RUBY_VERSION >= "1.9.2"
it "allows exploits through objects defined in YAML w/ !ruby/object via the :init_with method" do
backdoor = YAML.unsafe_load_file "spec/exploit.1.9.2.yaml"
expect(backdoor).to be_exploited_through_init_with
end
end
it "allows exploits through objects w/ sensitive instance variables defined in YAML w/ !ruby/object" do
backdoor = YAML.unsafe_load_file "spec/exploit.1.9.2.yaml"
expect(backdoor).to be_exploited_through_ivars
end
end
describe "safe_load_file" do
it "does NOT allow exploits through objects defined in YAML w/ !ruby/hash" do
object = YAML.safe_load_file "spec/exploit.1.9.3.yaml"
expect(object).not_to be_a(ExploitableBackDoor)
end
it "does NOT allow exploits through objects defined in YAML w/ !ruby/object" do
object = YAML.safe_load_file "spec/exploit.1.9.2.yaml"
expect(object).not_to be_a(ExploitableBackDoor)
end
it "returns false when parsing an empty file" do
expect(YAML.safe_load_file("spec/issue49.yml")).to eq(false)
end
end
describe "load" do
let(:options) { {} }
let (:arguments) {
if SafeYAML::MULTI_ARGUMENT_YAML_LOAD
["foo: bar", nil, options]
else
["foo: bar", options]
end
}
context "as long as a :default_mode has been specified" do
it "doesn't issue a warning for safe mode, since an explicit mode has been set" do
SafeYAML::OPTIONS[:default_mode] = :safe
expect(Kernel).not_to receive(:warn)
YAML.load(*arguments)
end
it "doesn't issue a warning for unsafe mode, since an explicit mode has been set" do
SafeYAML::OPTIONS[:default_mode] = :unsafe
expect(Kernel).not_to receive(:warn)
YAML.load(*arguments)
end
end
context "when the :safe options is specified" do
let(:safe_mode) { true }
let(:options) { { :safe => safe_mode } }
it "doesn't issue a warning" do
expect(Kernel).not_to receive(:warn)
YAML.load(*arguments)
end
it "calls #safe_load if the :safe option is set to true" do
expect(YAML).to receive(:safe_load)
YAML.load(*arguments)
end
context "when the :safe option is set to false" do
let(:safe_mode) { false }
it "calls #unsafe_load if the :safe option is set to false" do
expect(YAML).to receive(:unsafe_load)
YAML.load(*arguments)
end
end
end
it "issues a warning when the :safe option is omitted" do
silence_warnings do
expect(Kernel).to receive(:warn)
YAML.load(*arguments)
end
end
it "only issues a warning once (to avoid spamming an app's output)" do
silence_warnings do
expect(Kernel).to receive(:warn).once
2.times { YAML.load(*arguments) }
end
end
it "defaults to safe mode if the :safe option is omitted" do
silence_warnings do
expect(YAML).to receive(:safe_load)
YAML.load(*arguments)
end
end
context "with the default mode set to :unsafe" do
before :each do
SafeYAML::OPTIONS[:default_mode] = :unsafe
end
it "defaults to unsafe mode if the :safe option is omitted" do
silence_warnings do
expect(YAML).to receive(:unsafe_load)
YAML.load(*arguments)
end
end
it "calls #safe_load if the :safe option is set to true" do
expect(YAML).to receive(:safe_load)
YAML.load(*(arguments + [{ :safe => true }]))
end
end
end
describe "load_file" do
let(:filename) { "spec/exploit.1.9.2.yaml" } # doesn't really matter
it "issues a warning if the :safe option is omitted" do
silence_warnings do
expect(Kernel).to receive(:warn)
YAML.load_file(filename)
end
end
it "doesn't issue a warning as long as the :safe option is specified" do
expect(Kernel).not_to receive(:warn)
YAML.load_file(filename, :safe => true)
end
it "defaults to safe mode if the :safe option is omitted" do
silence_warnings do
expect(YAML).to receive(:safe_load_file)
YAML.load_file(filename)
end
end
it "calls #safe_load_file if the :safe option is set to true" do
expect(YAML).to receive(:safe_load_file)
YAML.load_file(filename, :safe => true)
end
it "calls #unsafe_load_file if the :safe option is set to false" do
expect(YAML).to receive(:unsafe_load_file)
YAML.load_file(filename, :safe => false)
end
context "with arbitrary object deserialization enabled by default" do
before :each do
SafeYAML::OPTIONS[:default_mode] = :unsafe
end
it "defaults to unsafe mode if the :safe option is omitted" do
silence_warnings do
expect(YAML).to receive(:unsafe_load_file)
YAML.load_file(filename)
end
end
it "calls #safe_load if the :safe option is set to true" do
expect(YAML).to receive(:safe_load_file)
YAML.load_file(filename, :safe => true)
end
end
it "handles files starting with --- (see issue #48)" do
expect(YAML.load_file("spec/issue48.txt", :safe => true)).to eq({
"title" => "Blah",
"key" => "value"
})
end
it "handles content starting with --- (see issue #48)" do
yaml = File.read("spec/issue48.txt")
expect(YAML.load(yaml, :safe => true)).to eq({
"title" => "Blah",
"key" => "value"
})
end
end
describe "whitelist!" do
context "not a class" do
it "should raise" do
expect { SafeYAML::whitelist! :foo }.to raise_error(/not a Class/)
expect(SafeYAML::OPTIONS[:whitelisted_tags]).to be_empty
end
end
context "anonymous class" do
it "should raise" do
expect { SafeYAML::whitelist! Class.new }.to raise_error(/cannot be anonymous/)
expect(SafeYAML::OPTIONS[:whitelisted_tags]).to be_empty
end
end
context "with a Class as its argument" do
it "should configure correctly" do
expect { SafeYAML::whitelist! OpenStruct }.to_not raise_error
expect(SafeYAML::OPTIONS[:whitelisted_tags].grep(/OpenStruct\Z/)).not_to be_empty
end
it "successfully deserializes the specified class" do
SafeYAML.whitelist!(OpenStruct)
# necessary for properly assigning OpenStruct attributes
SafeYAML::OPTIONS[:deserialize_symbols] = true
result = safe_load_round_trip(OpenStruct.new(:foo => "bar"))
expect(result).to be_a(OpenStruct)
expect(result.foo).to eq("bar")
end
it "works for ranges" do
SafeYAML.whitelist!(Range)
expect(safe_load_round_trip(1..10)).to eq(1..10)
end
it "works for regular expressions" do
SafeYAML.whitelist!(Regexp)
expect(safe_load_round_trip(/foo/)).to eq(/foo/)
end
it "works for multiple classes" do
SafeYAML.whitelist!(Range, Regexp)
expect(safe_load_round_trip([(1..10), /bar/])).to eq([(1..10), /bar/])
end
it "works for arbitrary Exception subclasses" do
class CustomException < Exception
attr_reader :custom_message
def initialize(custom_message)
@custom_message = custom_message
end
end
SafeYAML.whitelist!(CustomException)
ex = safe_load_round_trip(CustomException.new("blah"))
expect(ex).to be_a(CustomException)
expect(ex.custom_message).to eq("blah")
end
end
end
end
@@ -0,0 +1,42 @@
HERE = File.dirname(__FILE__) unless defined?(HERE)
ROOT = File.join(HERE, "..") unless defined?(ROOT)
$LOAD_PATH << File.join(ROOT, "lib")
$LOAD_PATH << File.join(HERE, "support")
require "yaml"
if ENV["YAMLER"] && defined?(YAML::ENGINE)
YAML::ENGINE.yamler = ENV["YAMLER"]
end
ruby_version = defined?(JRUBY_VERSION) ? "JRuby #{JRUBY_VERSION} in #{RUBY_VERSION} mode" : "Ruby #{RUBY_VERSION}"
yaml_engine = defined?(YAML::ENGINE) ? YAML::ENGINE.yamler : "syck"
libyaml_version = yaml_engine == "psych" && Psych.const_defined?("LIBYAML_VERSION", false) ? Psych::LIBYAML_VERSION : "N/A"
env_info = [
ruby_version,
"YAML: #{yaml_engine} (#{YAML::VERSION}) (libyaml: #{libyaml_version})",
"Monkeypatch: #{ENV['MONKEYPATCH_YAML']}"
]
puts env_info.join(", ")
# Caching references to these methods before loading safe_yaml in order to test
# that they aren't touched unless you actually require safe_yaml (see yaml_spec.rb).
ORIGINAL_YAML_LOAD = YAML.method(:load)
ORIGINAL_YAML_LOAD_FILE = YAML.method(:load_file)
require "safe_yaml/load"
require "ostruct"
require "hashie"
require "heredoc_unindent"
# Stolen from Rails:
# https://github.com/rails/rails/blob/3-2-stable/activesupport/lib/active_support/core_ext/kernel/reporting.rb#L10-25
def silence_warnings
$VERBOSE = nil; yield
ensure
$VERBOSE = true
end
require File.join(HERE, "resolver_specs")
@@ -0,0 +1,57 @@
require 'spec_helper'
require 'safe_yaml/store'
describe SafeYAML::Store do
let(:file) { 'spec/store.yaml' }
let(:content) { "--- \nfoo: 42\n:bar: \"party\"\n" }
before do
# Rewrite file on every test, as its contents are potentially modified by
# SafeYAML::Store#transaction
File.open(file, 'w') { |f| f.write(content) }
end
def expect_safe_load(options = {})
load_args = [content, options]
load_args.insert(1, nil) if SafeYAML::YAML_ENGINE == 'psych'
expect(SafeYAML).to receive(:load).with(*load_args).and_call_original
expect(YAML).not_to receive(:load)
end
let(:init_args) { [file] }
subject { described_class.new(*init_args) }
it 'should be a YAML::Store' do
expect(subject).to be_a(YAML::Store)
end
it 'should be a SafeYAML::Store' do
expect(subject).to be_a(SafeYAML::Store)
end
it 'should use SafeYAML.load instead of YAML.load' do
expect_safe_load
expect(subject.transaction { subject['foo'] }).to eq(42)
end
it 'preserves default SafeYAML behavior' do
expect(subject.transaction { subject[:bar] }).to eq(nil)
expect(subject.transaction { subject[':bar'] }).to eq('party')
end
describe 'with options' do
let(:init_args) { super().insert(2, :deserialize_symbols => true) }
it 'should accept options for SafeYAML.load' do
expect_safe_load(:deserialize_symbols => true)
expect(subject.transaction { subject[:bar] }).to eq('party')
end
end
end
@@ -0,0 +1,29 @@
class ExploitableBackDoor
def exploited?
@exploited_through_setter || @exploited_through_init_with || @exploited_through_ivars
end
def exploited_through_setter?
@exploited_through_setter
end
def exploited_through_init_with?
@exploited_through_init_with
end
def exploited_through_ivars?
self.instance_variables.any?
end
def init_with(command)
# Note: this is how bad this COULD be.
# system("#{command}")
@exploited_through_init_with = true
end
def []=(command, arguments)
# Note: this is how bad this COULD be.
# system("#{command} #{arguments}")
@exploited_through_setter = true
end
end
@@ -0,0 +1,10 @@
require "spec_helper"
if SafeYAML::YAML_ENGINE == "syck"
require "safe_yaml/syck_resolver"
describe SafeYAML::SyckResolver do
include ResolverSpecs
let(:resolver) { SafeYAML::SyckResolver.new }
end
end
@@ -0,0 +1,11 @@
require "spec_helper"
describe SafeYAML::Transform do
it "should return the same encoding when decoding Base64" do
value = "c3VyZS4="
decoded = SafeYAML::Transform.to_proper_type(value, false, "!binary")
expect(decoded).to eq("sure.")
expect(decoded.encoding).to eq(value.encoding) if decoded.respond_to?(:encoding)
end
end
@@ -0,0 +1,60 @@
require "spec_helper"
describe SafeYAML::Transform::ToDate do
it "returns true when the value matches a valid Date" do
expect(subject.transform?("2013-01-01")).to eq([true, Date.parse("2013-01-01")])
end
it "returns false when the value does not match a valid Date" do
expect(subject.transform?("foobar")).to be_falsey
end
it "returns false when the value does not end with a Date" do
expect(subject.transform?("2013-01-01\nNOT A DATE")).to be_falsey
end
it "returns false when the value does not begin with a Date" do
expect(subject.transform?("NOT A DATE\n2013-01-01")).to be_falsey
end
it "correctly parses the remaining formats of the YAML spec" do
equivalent_values = [
"2001-12-15T02:59:43.1Z", # canonical
"2001-12-14t21:59:43.10-05:00", # iso8601
"2001-12-14 21:59:43.10 -5", # space separated
"2001-12-15 2:59:43.10" # no time zone (Z)
]
equivalent_values.each do |value|
success, result = subject.transform?(value)
expect(success).to be_truthy
expect(result).to eq(Time.utc(2001, 12, 15, 2, 59, 43, 100000))
end
end
it "converts times to the local timezone" do
success, result = subject.transform?("2012-12-01 10:33:45 +11:00")
expect(success).to be_truthy
expect(result).to eq(Time.utc(2012, 11, 30, 23, 33, 45))
expect(result.gmt_offset).to eq(Time.local(2012, 11, 30).gmt_offset)
end
it "returns strings for invalid dates" do
expect(subject.transform?("0000-00-00")).to eq([true, "0000-00-00"])
expect(subject.transform?("2013-13-01")).to eq([true, "2013-13-01"])
expect(subject.transform?("2014-01-32")).to eq([true, "2014-01-32"])
end
it "returns strings for invalid date/times" do
expect(subject.transform?("0000-00-00 00:00:00 -0000")).to eq([true, "0000-00-00 00:00:00 -0000"])
expect(subject.transform?("2013-13-01 21:59:43 -05:00")).to eq([true, "2013-13-01 21:59:43 -05:00"])
expect(subject.transform?("2013-01-32 21:59:43 -05:00")).to eq([true, "2013-01-32 21:59:43 -05:00"])
expect(subject.transform?("2013-01-30 25:59:43 -05:00")).to eq([true, "2013-01-30 25:59:43 -05:00"])
expect(subject.transform?("2013-01-30 21:69:43 -05:00")).to eq([true, "2013-01-30 21:69:43 -05:00"])
# Interesting. It seems that in some older Ruby versions, the below actually parses successfully
# w/ DateTime.parse; but it fails w/ YAML.load. Whom to follow???
# subject.transform?("2013-01-30 21:59:63 -05:00").should == [true, "2013-01-30 21:59:63 -05:00"]
end
end
@@ -0,0 +1,42 @@
require "spec_helper"
describe SafeYAML::Transform::ToFloat do
it "returns true when the value matches a valid Float" do
expect(subject.transform?("20.00")).to eq([true, 20.0])
end
it "returns false when the value does not match a valid Float" do
expect(subject.transform?("foobar")).to be_falsey
end
it "returns false when the value spans multiple lines" do
expect(subject.transform?("20.00\nNOT A FLOAT")).to be_falsey
end
it "correctly parses all formats in the YAML spec" do
# canonical
expect(subject.transform?("6.8523015e+5")).to eq([true, 685230.15])
# exponentioal
expect(subject.transform?("685.230_15e+03")).to eq([true, 685230.15])
# fixed
expect(subject.transform?("685_230.15")).to eq([true, 685230.15])
# sexagesimal
expect(subject.transform?("190:20:30.15")).to eq([true, 685230.15])
# infinity
expect(subject.transform?("-.inf")).to eq([true, (-1.0 / 0.0)])
# not a number
# NOTE: can't use == here since NaN != NaN
success, result = subject.transform?(".NaN")
expect(success).to be_truthy; expect(result).to be_nan
end
# issue 29
it "returns false for the string '.'" do
expect(subject.transform?(".")).to be_falsey
end
end
@@ -0,0 +1,64 @@
require "spec_helper"
describe SafeYAML::Transform::ToInteger do
it "returns true when the value matches a valid Integer" do
expect(subject.transform?("10")).to eq([true, 10])
end
it "returns false when the value does not match a valid Integer" do
expect(subject.transform?("foobar")).to be_falsey
end
it "returns false when the value spans multiple lines" do
expect(subject.transform?("10\nNOT AN INTEGER")).to be_falsey
end
it "allows commas in the number" do
expect(subject.transform?("1,000")).to eq([true, 1000])
end
it "correctly parses numbers in octal format" do
expect(subject.transform?("010")).to eq([true, 8])
end
it "correctly parses numbers in hexadecimal format" do
expect(subject.transform?("0x1FF")).to eq([true, 511])
end
it "defaults to a string for a number that resembles octal format but is not" do
expect(subject.transform?("09")).to be_falsey
end
it "correctly parses 0 in decimal" do
expect(subject.transform?("0")).to eq([true, 0])
end
it "defaults to a string for a number that resembles hexadecimal format but is not" do
expect(subject.transform?("0x1G")).to be_falsey
end
it "correctly parses all formats in the YAML spec" do
# canonical
expect(subject.transform?("685230")).to eq([true, 685230])
# decimal
expect(subject.transform?("+685_230")).to eq([true, 685230])
# octal
expect(subject.transform?("02472256")).to eq([true, 685230])
# hexadecimal:
expect(subject.transform?("0x_0A_74_AE")).to eq([true, 685230])
# binary
expect(subject.transform?("0b1010_0111_0100_1010_1110")).to eq([true, 685230])
# sexagesimal
expect(subject.transform?("190:20:30")).to eq([true, 685230])
end
# see https://github.com/dtao/safe_yaml/pull/51
it "strips out underscores before parsing decimal values" do
expect(subject.transform?("_850_")).to eq([true, 850])
end
end
@@ -0,0 +1,51 @@
require "spec_helper"
describe SafeYAML::Transform::ToSymbol do
def with_symbol_deserialization_value(value)
symbol_deserialization_flag = SafeYAML::OPTIONS[:deserialize_symbols]
SafeYAML::OPTIONS[:deserialize_symbols] = value
yield
ensure
SafeYAML::OPTIONS[:deserialize_symbols] = symbol_deserialization_flag
end
def with_symbol_deserialization(&block)
with_symbol_deserialization_value(true, &block)
end
def without_symbol_deserialization(&block)
with_symbol_deserialization_value(false, &block)
end
it "returns true when the value matches a valid Symbol" do
with_symbol_deserialization { expect(subject.transform?(":foo")[0]).to be_truthy }
end
it "returns true when the value matches a valid String+Symbol" do
with_symbol_deserialization { expect(subject.transform?(':"foo"')[0]).to be_truthy }
end
it "returns true when the value matches a valid String+Symbol with 's" do
with_symbol_deserialization { expect(subject.transform?(":'foo'")[0]).to be_truthy }
end
it "returns true when the value has special characters and is wrapped in a String" do
with_symbol_deserialization { expect(subject.transform?(':"foo.bar"')[0]).to be_truthy }
end
it "returns false when symbol deserialization is disabled" do
without_symbol_deserialization { expect(subject.transform?(":foo")).to be_falsey }
end
it "returns false when the value does not match a valid Symbol" do
with_symbol_deserialization { expect(subject.transform?("foo")).to be_falsey }
end
it "returns false when the symbol does not begin the line" do
with_symbol_deserialization do
expect(subject.transform?("NOT A SYMBOL\n:foo")).to be_falsey
end
end
end
@@ -0,0 +1,15 @@
# See https://github.com/dtao/safe_yaml/issues/47
require "spec_helper"
describe YAML do
context "when you've only required safe_yaml/load", :libraries => true do
it "YAML.load doesn't get monkey patched" do
expect(YAML.method(:load)).to eq(ORIGINAL_YAML_LOAD)
end
it "YAML.load_file doesn't get monkey patched" do
expect(YAML.method(:load_file)).to eq(ORIGINAL_YAML_LOAD_FILE)
end
end
end