Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update terminologies #93

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -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
-----

Expand Down
24 changes: 12 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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:
Expand All @@ -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.

Expand Down
20 changes: 10 additions & 10 deletions lib/safe_yaml/load.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down Expand Up @@ -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

Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions lib/safe_yaml/resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions lib/safe_yaml/syck_node_monkeypatch.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -9,24 +9,24 @@
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)

ensure
@@safe_transform_depth -= 1
if @@safe_transform_depth == 0
@@safe_transform_whitelist = nil
@@safe_transform_allowlist = nil
end
end
end
Expand Down
62 changes: 31 additions & 31 deletions spec/safe_yaml_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -77,17 +77,17 @@ 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.
YAML.safe_load(*arguments) rescue nil
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
Expand Down Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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"]
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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)
Expand Down