safe_yaml-1.0.4/0000755000004100000410000000000012412360563013506 5ustar www-datawww-datasafe_yaml-1.0.4/Rakefile0000644000004100000410000000126012412360563015152 0ustar www-datawww-datarequire "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 safe_yaml-1.0.4/bin/0000755000004100000410000000000012412360563014256 5ustar www-datawww-datasafe_yaml-1.0.4/bin/safe_yaml0000755000004100000410000000403412412360563016145 0ustar www-datawww-data#!/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=", "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 safe_yaml-1.0.4/Gemfile0000644000004100000410000000023112412360563014775 0ustar www-datawww-datasource "https://rubygems.org" gemspec group :development do gem "hashie" gem "heredoc_unindent" gem "rake" gem "rspec" gem "travis-lint" end safe_yaml-1.0.4/bundle_install_all_ruby_versions.sh0000755000004100000410000000036112412360563022665 0ustar www-datawww-data#!/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 safe_yaml-1.0.4/LICENSE.txt0000644000004100000410000000205012412360563015326 0ustar www-datawww-dataCopyright (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. safe_yaml-1.0.4/spec/0000755000004100000410000000000012412360563014440 5ustar www-datawww-datasafe_yaml-1.0.4/spec/safe_yaml_spec.rb0000644000004100000410000005454712412360563017756 0ustar www-datawww-datarequire "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 safe_yaml-1.0.4/spec/issue49.yml0000644000004100000410000000000012412360563016456 0ustar www-datawww-datasafe_yaml-1.0.4/spec/issue48.txt0000644000004100000410000000032212412360563016502 0ustar www-datawww-data--- 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! --- safe_yaml-1.0.4/spec/psych_resolver_spec.rb0000644000004100000410000000033612412360563021050 0ustar www-datawww-datarequire "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 safe_yaml-1.0.4/spec/resolver_specs.rb0000644000004100000410000001705112412360563020027 0ustar www-datawww-datamodule 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 safe_yaml-1.0.4/spec/spec_helper.rb0000644000004100000410000000253612412360563017264 0ustar www-datawww-dataHERE = 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") safe_yaml-1.0.4/spec/transform/0000755000004100000410000000000012412360563016453 5ustar www-datawww-datasafe_yaml-1.0.4/spec/transform/base64_spec.rb0000644000004100000410000000053212412360563021076 0ustar www-datawww-datarequire "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 safe_yaml-1.0.4/spec/transform/to_integer_spec.rb0000644000004100000410000000361712412360563022160 0ustar www-datawww-datarequire "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 safe_yaml-1.0.4/spec/transform/to_date_spec.rb0000644000004100000410000000476212412360563021442 0ustar www-datawww-datarequire "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 safe_yaml-1.0.4/spec/transform/to_float_spec.rb0000644000004100000410000000233612412360563021625 0ustar www-datawww-datarequire "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 safe_yaml-1.0.4/spec/transform/to_symbol_spec.rb0000644000004100000410000000327712412360563022032 0ustar www-datawww-datarequire "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 safe_yaml-1.0.4/spec/libyaml_checker_spec.rb0000644000004100000410000000423212412360563021115 0ustar www-datawww-datarequire "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 safe_yaml-1.0.4/spec/yaml_spec.rb0000644000004100000410000000065112412360563016743 0ustar www-datawww-data# 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 safe_yaml-1.0.4/spec/exploit.1.9.2.yaml0000644000004100000410000000005612412360563017457 0ustar www-datawww-data--- !ruby/object:ExploitableBackDoor foo: bar safe_yaml-1.0.4/spec/syck_resolver_spec.rb0000644000004100000410000000033212412360563020667 0ustar www-datawww-datarequire "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 safe_yaml-1.0.4/spec/exploit.1.9.3.yaml0000644000004100000410000000005412412360563017456 0ustar www-datawww-data--- !ruby/hash:ExploitableBackDoor foo: bar safe_yaml-1.0.4/spec/support/0000755000004100000410000000000012412360563016154 5ustar www-datawww-datasafe_yaml-1.0.4/spec/support/exploitable_back_door.rb0000644000004100000410000000122112412360563023010 0ustar www-datawww-dataclass 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 safe_yaml-1.0.4/.travis.yml0000644000004100000410000000116512412360563015622 0ustar www-datawww-datalanguage: 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 safe_yaml-1.0.4/lib/0000755000004100000410000000000012412360563014254 5ustar www-datawww-datasafe_yaml-1.0.4/lib/safe_yaml/0000755000004100000410000000000012412360563016214 5ustar www-datawww-datasafe_yaml-1.0.4/lib/safe_yaml/transform.rb0000644000004100000410000000211112412360563020547 0ustar www-datawww-datarequire '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 safe_yaml-1.0.4/lib/safe_yaml/syck_resolver.rb0000644000004100000410000000117212412360563021434 0ustar www-datawww-datamodule 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 safe_yaml-1.0.4/lib/safe_yaml/psych_handler.rb0000644000004100000410000000442412412360563021370 0ustar www-datawww-datarequire "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 safe_yaml-1.0.4/lib/safe_yaml/syck_hack.rb0000644000004100000410000000215712412360563020505 0ustar www-datawww-data# 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 safe_yaml-1.0.4/lib/safe_yaml/resolver.rb0000644000004100000410000000515412412360563020407 0ustar www-datawww-datamodule 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 safe_yaml-1.0.4/lib/safe_yaml/transform/0000755000004100000410000000000012412360563020227 5ustar www-datawww-datasafe_yaml-1.0.4/lib/safe_yaml/transform/transformation_map.rb0000644000004100000410000000212012412360563024452 0ustar www-datawww-datamodule 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 safe_yaml-1.0.4/lib/safe_yaml/transform/to_date.rb0000644000004100000410000000053012412360563022171 0ustar www-datawww-datamodule 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 safe_yaml-1.0.4/lib/safe_yaml/transform/to_boolean.rb0000644000004100000410000000070312412360563022675 0ustar www-datawww-datamodule 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 safe_yaml-1.0.4/lib/safe_yaml/transform/to_symbol.rb0000644000004100000410000000062212412360563022563 0ustar www-datawww-datamodule 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 safe_yaml-1.0.4/lib/safe_yaml/transform/to_integer.rb0000644000004100000410000000147412412360563022721 0ustar www-datawww-datamodule 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 safe_yaml-1.0.4/lib/safe_yaml/transform/to_nil.rb0000644000004100000410000000055612412360563022046 0ustar www-datawww-datamodule 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 safe_yaml-1.0.4/lib/safe_yaml/transform/to_float.rb0000644000004100000410000000154212412360563022365 0ustar www-datawww-datamodule 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 safe_yaml-1.0.4/lib/safe_yaml/version.rb0000644000004100000410000000005012412360563020221 0ustar www-datawww-datamodule SafeYAML VERSION = "1.0.4" end safe_yaml-1.0.4/lib/safe_yaml/load.rb0000644000004100000410000001274712412360563017473 0ustar www-datawww-datarequire "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 safe_yaml-1.0.4/lib/safe_yaml/parse/0000755000004100000410000000000012412360563017326 5ustar www-datawww-datasafe_yaml-1.0.4/lib/safe_yaml/parse/date.rb0000644000004100000410000000263412412360563020575 0ustar www-datawww-datamodule 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 safe_yaml-1.0.4/lib/safe_yaml/parse/hexadecimal.rb0000644000004100000410000000041612412360563022120 0ustar www-datawww-datamodule 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 safe_yaml-1.0.4/lib/safe_yaml/parse/sexagesimal.rb0000644000004100000410000000130312412360563022152 0ustar www-datawww-datamodule 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 safe_yaml-1.0.4/lib/safe_yaml/libyaml_checker.rb0000644000004100000410000000216012412360563021655 0ustar www-datawww-datarequire "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 safe_yaml-1.0.4/lib/safe_yaml/safe_to_ruby_visitor.rb0000644000004100000410000000126212412360563023002 0ustar www-datawww-datamodule 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 safe_yaml-1.0.4/lib/safe_yaml/psych_resolver.rb0000644000004100000410000000205312412360563021610 0ustar www-datawww-datamodule 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 safe_yaml-1.0.4/lib/safe_yaml/deep.rb0000644000004100000410000000123312412360563017455 0ustar www-datawww-datamodule 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 safe_yaml-1.0.4/lib/safe_yaml/syck_node_monkeypatch.rb0000644000004100000410000000302612412360563023122 0ustar www-datawww-data# 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 safe_yaml-1.0.4/lib/safe_yaml.rb0000644000004100000410000000513212412360563016542 0ustar www-datawww-datarequire "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 safe_yaml-1.0.4/metadata.yml0000644000004100000410000000560012412360563016012 0ustar www-datawww-data--- !ruby/object:Gem::Specification name: safe_yaml version: !ruby/object:Gem::Version version: 1.0.4 platform: ruby authors: - Dan Tao autorequire: bindir: bin cert_chain: [] date: 2014-09-28 00:00:00.000000000 Z dependencies: [] description: Parse YAML safely email: daniel.tao@gmail.com executables: - safe_yaml extensions: [] extra_rdoc_files: [] files: - ".gitignore" - ".travis.yml" - CHANGES.md - Gemfile - LICENSE.txt - README.md - Rakefile - bin/safe_yaml - bundle_install_all_ruby_versions.sh - lib/safe_yaml.rb - lib/safe_yaml/deep.rb - lib/safe_yaml/libyaml_checker.rb - lib/safe_yaml/load.rb - lib/safe_yaml/parse/date.rb - lib/safe_yaml/parse/hexadecimal.rb - lib/safe_yaml/parse/sexagesimal.rb - lib/safe_yaml/psych_handler.rb - lib/safe_yaml/psych_resolver.rb - lib/safe_yaml/resolver.rb - lib/safe_yaml/safe_to_ruby_visitor.rb - lib/safe_yaml/syck_hack.rb - lib/safe_yaml/syck_node_monkeypatch.rb - lib/safe_yaml/syck_resolver.rb - lib/safe_yaml/transform.rb - lib/safe_yaml/transform/to_boolean.rb - lib/safe_yaml/transform/to_date.rb - lib/safe_yaml/transform/to_float.rb - lib/safe_yaml/transform/to_integer.rb - lib/safe_yaml/transform/to_nil.rb - lib/safe_yaml/transform/to_symbol.rb - lib/safe_yaml/transform/transformation_map.rb - lib/safe_yaml/version.rb - run_specs_all_ruby_versions.sh - safe_yaml.gemspec - spec/exploit.1.9.2.yaml - spec/exploit.1.9.3.yaml - spec/issue48.txt - spec/issue49.yml - spec/libyaml_checker_spec.rb - spec/psych_resolver_spec.rb - spec/resolver_specs.rb - spec/safe_yaml_spec.rb - spec/spec_helper.rb - spec/support/exploitable_back_door.rb - spec/syck_resolver_spec.rb - spec/transform/base64_spec.rb - spec/transform/to_date_spec.rb - spec/transform/to_float_spec.rb - spec/transform/to_integer_spec.rb - spec/transform/to_symbol_spec.rb - spec/yaml_spec.rb homepage: https://github.com/dtao/safe_yaml licenses: - MIT metadata: {} post_install_message: rdoc_options: [] require_paths: - lib required_ruby_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: 1.8.7 required_rubygems_version: !ruby/object:Gem::Requirement requirements: - - ">=" - !ruby/object:Gem::Version version: '0' requirements: [] rubyforge_project: rubygems_version: 2.1.11 signing_key: specification_version: 4 summary: SameYAML provides an alternative implementation of YAML.load suitable for accepting user input in Ruby applications. test_files: - spec/exploit.1.9.2.yaml - spec/exploit.1.9.3.yaml - spec/issue48.txt - spec/issue49.yml - spec/libyaml_checker_spec.rb - spec/psych_resolver_spec.rb - spec/resolver_specs.rb - spec/safe_yaml_spec.rb - spec/spec_helper.rb - spec/support/exploitable_back_door.rb - spec/syck_resolver_spec.rb - spec/transform/base64_spec.rb - spec/transform/to_date_spec.rb - spec/transform/to_float_spec.rb - spec/transform/to_integer_spec.rb - spec/transform/to_symbol_spec.rb - spec/yaml_spec.rb safe_yaml-1.0.4/safe_yaml.gemspec0000644000004100000410000000137412412360563017020 0ustar www-datawww-data# -*- 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 safe_yaml-1.0.4/run_specs_all_ruby_versions.sh0000755000004100000410000000113712412360563021671 0ustar www-datawww-data#!/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 safe_yaml-1.0.4/.gitignore0000644000004100000410000000001512412360563015472 0ustar www-datawww-dataGemfile.lock safe_yaml-1.0.4/CHANGES.md0000644000004100000410000000407712412360563015110 0ustar www-datawww-data1.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 releasesafe_yaml-1.0.4/README.md0000644000004100000410000002454412412360563014776 0ustar www-datawww-dataSafeYAML ======== [![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! => #> 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) => #"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.