diff --git a/CHANGES.md b/CHANGES.md index 2587b47..1bd3887 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,8 @@ +- Breaking change. Update terminologies. + * Replace `SafeYAML::whitelist!` with `SafeYAML::permit!` + * Replace `SafeYAML::whitelist_class!` with `SafeYAML::permit_class!` + * Option `whitelisted_tags` changed to `permitted_tags` + 1.0.2 ----- diff --git a/README.md b/README.md index a7b7bdc..58184d2 100644 --- a/README.md +++ b/README.md @@ -91,11 +91,11 @@ The most important option is the `:safe` option (default: `true`), which control - `: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. +- `:permitted_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 "Permitting 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. +- `:custom_initializers`: Similar to the `:permitted_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. +- `: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 permitted, 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. @@ -126,25 +126,25 @@ The way that SafeYAML works is by restricting the kinds of objects that can be d 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 +Permitting 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. +SafeYAML supports permitting 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.: +The easiest way to permit types is by calling `SafeYAML.permit!`, which can accept a variable number of safe types, e.g.: ```ruby -SafeYAML.whitelist!(Foo, Bar) +SafeYAML.permit!(Foo, Bar) ``` -You can also whitelist YAML *tags* via the `:whitelisted_tags` option: +You can also permit YAML *tags* via the `:permitted_tags` option: ```ruby # Using Syck -SafeYAML::OPTIONS[:whitelisted_tags] = ["tag:ruby.yaml.org,2002:object:OpenStruct"] +SafeYAML::OPTIONS[:permitted_tags] = ["tag:ruby.yaml.org,2002:object:OpenStruct"] # Using Psych -SafeYAML::OPTIONS[:whitelisted_tags] = ["!ruby/object:OpenStruct"] +SafeYAML::OPTIONS[:permitted_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: @@ -170,9 +170,9 @@ Also be aware that some Ruby libraries, particularly those requiring inter-proce - [**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 + 2. permit some of the types in your serialized data via `SafeYAML.permit!` or the `:permitted_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. +- [**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.permit!` to permit 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. diff --git a/lib/safe_yaml/load.rb b/lib/safe_yaml/load.rb index 5ea0f60..700bc6b 100644 --- a/lib/safe_yaml/load.rb +++ b/lib/safe_yaml/load.rb @@ -30,7 +30,7 @@ module SafeYAML :default_mode => nil, :suppress_warnings => false, :deserialize_symbols => false, - :whitelisted_tags => [], + :permitted_tags => [], :custom_initializers => {}, :raise_on_unknown_tag => false }) @@ -65,27 +65,27 @@ def restore_defaults! 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) + if options[:raise_on_unknown_tag] && !options[:permitted_tags].include?(tag) && !tag_is_explicitly_trusted?(tag) raise "Unknown YAML tag '#{tag}'" end end - def whitelist!(*classes) + def permit!(*classes) classes.each do |klass| - whitelist_class!(klass) + permit_class!(klass) end end - def whitelist_class!(klass) + def permit_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. + # Permit any built-in YAML tags supplied by Syck or Psych. predefined_tag = PREDEFINED_TAGS[klass] if predefined_tag - OPTIONS[:whitelisted_tags] << predefined_tag + OPTIONS[:permitted_tags] << predefined_tag return end @@ -97,7 +97,7 @@ def whitelist_class!(klass) 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}" + OPTIONS[:permitted_tags] << "#{tag_prefix}:#{klass_name}" end if YAML_ENGINE == "psych" @@ -132,9 +132,9 @@ def tag_is_explicitly_trusted?(tag) 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 + # If the user hasn't permitted any tags, we can go with this implementation which is # significantly faster. - if (options && options[:whitelisted_tags] || SafeYAML::OPTIONS[:whitelisted_tags]).empty? + if (options && options[:permitted_tags] || SafeYAML::OPTIONS[:permitted_tags]).empty? safe_handler = SafeYAML::PsychHandler.new(options) do |result| return result end diff --git a/lib/safe_yaml/resolver.rb b/lib/safe_yaml/resolver.rb index e4de157..388ed95 100644 --- a/lib/safe_yaml/resolver.rb +++ b/lib/safe_yaml/resolver.rb @@ -2,14 +2,14 @@ module SafeYAML class Resolver def initialize(options) @options = SafeYAML::OPTIONS.merge(options || {}) - @whitelist = @options[:whitelisted_tags] || [] + @allowlist = @options[:permitted_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)) + return self.native_resolve(node) if tag_is_permitted?(self.get_node_tag(node)) case self.get_node_type(node) when :root @@ -65,8 +65,8 @@ def get_and_check_node_tag(node) tag end - def tag_is_whitelisted?(tag) - @whitelist.include?(tag) + def tag_is_permitted?(tag) + @allowlist.include?(tag) end def options diff --git a/lib/safe_yaml/syck_node_monkeypatch.rb b/lib/safe_yaml/syck_node_monkeypatch.rb index c026376..c1c7a17 100644 --- a/lib/safe_yaml/syck_node_monkeypatch.rb +++ b/lib/safe_yaml/syck_node_monkeypatch.rb @@ -1,5 +1,5 @@ # 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 +# Permitting 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 @@ -9,16 +9,16 @@ monkeypatch = <<-EORUBY class Node @@safe_transform_depth = 0 - @@safe_transform_whitelist = nil + @@safe_transform_allowlist = nil def safe_transform(options={}) begin @@safe_transform_depth += 1 - @@safe_transform_whitelist ||= options[:whitelisted_tags] + @@safe_transform_allowlist ||= options[:permitted_tags] if self.type_id SafeYAML.tag_safety_check!(self.type_id, options) - return unsafe_transform if @@safe_transform_whitelist.include?(self.type_id) + return unsafe_transform if @@safe_transform_allowlist.include?(self.type_id) end SafeYAML::SyckResolver.new.resolve_node(self) @@ -26,7 +26,7 @@ def safe_transform(options={}) ensure @@safe_transform_depth -= 1 if @@safe_transform_depth == 0 - @@safe_transform_whitelist = nil + @@safe_transform_allowlist = nil end end end diff --git a/spec/safe_yaml_spec.rb b/spec/safe_yaml_spec.rb index aa701a4..f2b6f8f 100644 --- a/spec/safe_yaml_spec.rb +++ b/spec/safe_yaml_spec.rb @@ -42,12 +42,12 @@ def safe_load_round_trip(object, options={}) expect(backdoor).to be_exploited_through_ivars end - context "with special whitelisted tags defined" do + context "with special permitted tags defined" do before :each do - SafeYAML::whitelist!(OpenStruct) + SafeYAML::permit!(OpenStruct) end - it "effectively ignores the whitelist (since everything is whitelisted)" do + it "effectively ignores those already been permitted" do result = YAML.unsafe_load <<-YAML.unindent --- !ruby/object:OpenStruct table: @@ -77,7 +77,7 @@ def safe_load_round_trip(object, options={}) let(:options) { nil } let(:arguments) { ["foo: bar", nil, options] } - context "when no tags are whitelisted" do + context "when no tags are permitted" 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. @@ -85,9 +85,9 @@ def safe_load_round_trip(object, options={}) end end - context "when whitelisted tags are specified" do + context "when permitted tags are specified" do let(:options) { - { :whitelisted_tags => ["foo"] } + { :permitted_tags => ["foo"] } } it "instead uses Psych to construct a full tree before examining the nodes" do @@ -307,27 +307,27 @@ def safe_load_round_trip(object, options={}) end end - context "with special whitelisted tags defined" do + context "with special permitted tags defined" do before :each do - SafeYAML::whitelist!(OpenStruct) + SafeYAML::permit!(OpenStruct) # Necessary for deserializing OpenStructs properly. SafeYAML::OPTIONS[:deserialize_symbols] = true end - it "will allow objects to be deserialized for whitelisted tags" do + it "will allow objects to be deserialized for permitted 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 + it "will not deserialize objects without permitted 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 + it "will not allow non-permitted objects to be embedded within objects with permitted tags" do result = YAML.safe_load <<-YAML.unindent --- !ruby/object:OpenStruct table: @@ -349,7 +349,7 @@ def safe_load_round_trip(object, options={}) SafeYAML.restore_defaults! end - it "raises an exception if a non-nil, non-whitelisted tag is encountered" do + it "raises an exception if a non-nil, non-permitted tag is encountered" do expect { YAML.safe_load <<-YAML.unindent --- !ruby/object:Unknown @@ -369,7 +369,7 @@ def safe_load_round_trip(object, options={}) }.to raise_error end - it "does not raise an exception as long as all tags are whitelisted" do + it "does not raise an exception as long as all tags are permitted" do result = YAML.safe_load <<-YAML.unindent --- !ruby/object:OpenStruct table: @@ -401,14 +401,14 @@ def safe_load_round_trip(object, options={}) expect(result).to eq("foo") end - context "with whitelisted custom class" do + context "with permitted custom class" do class SomeClass attr_accessor :foo end let(:instance) { SomeClass.new } before do - SafeYAML::whitelist!(SomeClass) + SafeYAML::permit!(SomeClass) instance.foo = 'with trailing whitespace: ' end @@ -445,11 +445,11 @@ class SomeClass end end - context "(or, for example, when certain tags are whitelisted)" do + context "(or, for example, when certain tags are permitted)" do let(:default_options) { { :deserialize_symbols => true, - :whitelisted_tags => SafeYAML::YAML_ENGINE == "psych" ? + :permitted_tags => SafeYAML::YAML_ENGINE == "psych" ? ["!ruby/object:OpenStruct"] : ["tag:ruby.yaml.org,2002:object:OpenStruct"] } @@ -462,10 +462,10 @@ class SomeClass 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 => []) + result = safe_load_round_trip(OpenStruct.new(:foo => "bar"), :permitted_tags => []) expect(result).to eq({ "table" => { :foo => "bar" } }) - result = safe_load_round_trip(OpenStruct.new(:foo => "bar"), :deserialize_symbols => false, :whitelisted_tags => []) + result = safe_load_round_trip(OpenStruct.new(:foo => "bar"), :deserialize_symbols => false, :permitted_tags => []) expect(result).to eq({ "table" => { ":foo" => "bar" } }) end end @@ -664,29 +664,29 @@ class SomeClass end end - describe "whitelist!" do + describe "permit!" 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 + expect { SafeYAML::permit! :foo }.to raise_error(/not a Class/) + expect(SafeYAML::OPTIONS[:permitted_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 + expect { SafeYAML::permit! Class.new }.to raise_error(/cannot be anonymous/) + expect(SafeYAML::OPTIONS[:permitted_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 + expect { SafeYAML::permit! OpenStruct }.to_not raise_error + expect(SafeYAML::OPTIONS[:permitted_tags].grep(/OpenStruct\Z/)).not_to be_empty end it "successfully deserializes the specified class" do - SafeYAML.whitelist!(OpenStruct) + SafeYAML.permit!(OpenStruct) # necessary for properly assigning OpenStruct attributes SafeYAML::OPTIONS[:deserialize_symbols] = true @@ -697,17 +697,17 @@ class SomeClass end it "works for ranges" do - SafeYAML.whitelist!(Range) + SafeYAML.permit!(Range) expect(safe_load_round_trip(1..10)).to eq(1..10) end it "works for regular expressions" do - SafeYAML.whitelist!(Regexp) + SafeYAML.permit!(Regexp) expect(safe_load_round_trip(/foo/)).to eq(/foo/) end it "works for multiple classes" do - SafeYAML.whitelist!(Range, Regexp) + SafeYAML.permit!(Range, Regexp) expect(safe_load_round_trip([(1..10), /bar/])).to eq([(1..10), /bar/]) end @@ -720,7 +720,7 @@ def initialize(custom_message) end end - SafeYAML.whitelist!(CustomException) + SafeYAML.permit!(CustomException) ex = safe_load_round_trip(CustomException.new("blah")) expect(ex).to be_a(CustomException)