b8524a
From 7ceafcbdf5bd2155704839f97b869e689f66feeb Mon Sep 17 00:00:00 2001
b8524a
From: tenderlove <tenderlove@b2dd03c8-39d4-4d8f-98ff-823fe69b080e>
b8524a
Date: Tue, 14 May 2013 17:26:41 +0000
b8524a
Subject: [PATCH] * ext/psych/lib/psych.rb: Adding Psych.safe_load for loading
b8524a
 a user   defined, restricted subset of Ruby object types. *
b8524a
 ext/psych/lib/psych/class_loader.rb: A class loader for   encapsulating the
b8524a
 logic for which objects are allowed to be   deserialized. *
b8524a
 ext/psych/lib/psych/deprecated.rb: Changes to use the class loader *
b8524a
 ext/psych/lib/psych/exception.rb: ditto * ext/psych/lib/psych/json/stream.rb:
b8524a
 ditto * ext/psych/lib/psych/nodes/node.rb: ditto *
b8524a
 ext/psych/lib/psych/scalar_scanner.rb: ditto * ext/psych/lib/psych/stream.rb:
b8524a
 ditto * ext/psych/lib/psych/streaming.rb: ditto *
b8524a
 ext/psych/lib/psych/visitors/json_tree.rb: ditto *
b8524a
 ext/psych/lib/psych/visitors/to_ruby.rb: ditto *
b8524a
 ext/psych/lib/psych/visitors/yaml_tree.rb: ditto * ext/psych/psych_to_ruby.c:
b8524a
 ditto * test/psych/helper.rb: ditto * test/psych/test_safe_load.rb: tests for
b8524a
 restricted subset. * test/psych/test_scalar_scanner.rb: ditto *
b8524a
 test/psych/visitors/test_to_ruby.rb: ditto *
b8524a
 test/psych/visitors/test_yaml_tree.rb: ditto
b8524a
b8524a
git-svn-id: svn+ssh://ci.ruby-lang.org/ruby/trunk@40750 b2dd03c8-39d4-4d8f-98ff-823fe69b080e
b8524a
---
b8524a
 ChangeLog                                 |  24 +++++++
b8524a
 ext/psych/lib/psych.rb                    |  57 +++++++++++++++--
b8524a
 ext/psych/lib/psych/class_loader.rb       | 101 ++++++++++++++++++++++++++++++
b8524a
 ext/psych/lib/psych/deprecated.rb         |   3 +-
b8524a
 ext/psych/lib/psych/exception.rb          |   6 ++
b8524a
 ext/psych/lib/psych/json/stream.rb        |   1 +
b8524a
 ext/psych/lib/psych/nodes/node.rb         |   4 +-
b8524a
 ext/psych/lib/psych/scalar_scanner.rb     |  19 +++---
b8524a
 ext/psych/lib/psych/stream.rb             |   1 +
b8524a
 ext/psych/lib/psych/streaming.rb          |  15 +++--
b8524a
 ext/psych/lib/psych/visitors/json_tree.rb |   7 ++-
b8524a
 ext/psych/lib/psych/visitors/to_ruby.rb   |  79 +++++++++++++----------
b8524a
 ext/psych/lib/psych/visitors/yaml_tree.rb |  13 +++-
b8524a
 ext/psych/psych_to_ruby.c                 |   4 +-
b8524a
 test/psych/helper.rb                      |   2 +-
b8524a
 test/psych/test_safe_load.rb              |  97 ++++++++++++++++++++++++++++
b8524a
 test/psych/test_scalar_scanner.rb         |   2 +-
b8524a
 test/psych/visitors/test_to_ruby.rb       |   4 +-
b8524a
 test/psych/visitors/test_yaml_tree.rb     |   4 +-
b8524a
 19 files changed, 383 insertions(+), 60 deletions(-)
b8524a
 create mode 100644 ext/psych/lib/psych/class_loader.rb
b8524a
 create mode 100644 test/psych/test_safe_load.rb
b8524a
b8524a
diff --git a/ChangeLog b/ChangeLog
b8524a
index be56f61d3a19..e8ad02a53921 100644
b8524a
--- a/ChangeLog
b8524a
+++ b/ChangeLog
b8524a
@@ -3137,6 +3137,30 @@
b8524a
 
b8524a
 	* include/ruby/intern.h: should include sys/time.h for struct timeval
b8524a
 	  if it exists. [ruby-list:49363]
b8524a
+
b8524a
+Wed May 15 02:22:16 2013  Aaron Patterson <aaron@tenderlovemaking.com>
b8524a
+
b8524a
+	* ext/psych/lib/psych.rb: Adding Psych.safe_load for loading a user
b8524a
+	  defined, restricted subset of Ruby object types.
b8524a
+	* ext/psych/lib/psych/class_loader.rb: A class loader for
b8524a
+	  encapsulating the logic for which objects are allowed to be
b8524a
+	  deserialized.
b8524a
+	* ext/psych/lib/psych/deprecated.rb: Changes to use the class loader
b8524a
+	* ext/psych/lib/psych/exception.rb: ditto
b8524a
+	* ext/psych/lib/psych/json/stream.rb: ditto
b8524a
+	* ext/psych/lib/psych/nodes/node.rb: ditto
b8524a
+	* ext/psych/lib/psych/scalar_scanner.rb: ditto
b8524a
+	* ext/psych/lib/psych/stream.rb: ditto
b8524a
+	* ext/psych/lib/psych/streaming.rb: ditto
b8524a
+	* ext/psych/lib/psych/visitors/json_tree.rb: ditto
b8524a
+	* ext/psych/lib/psych/visitors/to_ruby.rb: ditto
b8524a
+	* ext/psych/lib/psych/visitors/yaml_tree.rb: ditto
b8524a
+	* ext/psych/psych_to_ruby.c: ditto
b8524a
+	* test/psych/helper.rb: ditto
b8524a
+	* test/psych/test_safe_load.rb: tests for restricted subset.
b8524a
+	* test/psych/test_scalar_scanner.rb: ditto
b8524a
+	* test/psych/visitors/test_to_ruby.rb: ditto
b8524a
+	* test/psych/visitors/test_yaml_tree.rb: ditto
b8524a
 
b8524a
 Tue May 14 20:21:41 2013  Eric Hodel  <drbrain@segment7.net>
b8524a
 
b8524a
diff --git a/ext/psych/lib/psych.rb b/ext/psych/lib/psych.rb
b8524a
index 66a0641f39d8..711b3c1377dc 100644
b8524a
--- a/ext/psych/lib/psych.rb
b8524a
+++ b/ext/psych/lib/psych.rb
b8524a
@@ -124,6 +124,55 @@ def self.load yaml, filename = nil
b8524a
     result ? result.to_ruby : result
b8524a
   end
b8524a
 
b8524a
+  ###
b8524a
+  # Safely load the yaml string in +yaml+.  By default, only the following
b8524a
+  # classes are allowed to be deserialized:
b8524a
+  #
b8524a
+  # * TrueClass
b8524a
+  # * FalseClass
b8524a
+  # * NilClass
b8524a
+  # * Numeric
b8524a
+  # * String
b8524a
+  # * Array
b8524a
+  # * Hash
b8524a
+  #
b8524a
+  # Recursive data structures are not allowed by default.  Arbitrary classes
b8524a
+  # can be allowed by adding those classes to the +whitelist+.  They are
b8524a
+  # additive.  For example, to allow Date deserialization:
b8524a
+  #
b8524a
+  #   Psych.safe_load(yaml, [Date])
b8524a
+  #
b8524a
+  # Now the Date class can be loaded in addition to the classes listed above.
b8524a
+  #
b8524a
+  # Aliases can be explicitly allowed by changing the +aliases+ parameter.
b8524a
+  # For example:
b8524a
+  #
b8524a
+  #   x = []
b8524a
+  #   x << x
b8524a
+  #   yaml = Psych.dump x
b8524a
+  #   Psych.safe_load yaml               # => raises an exception
b8524a
+  #   Psych.safe_load yaml, [], [], true # => loads the aliases
b8524a
+  #
b8524a
+  # A Psych::DisallowedClass exception will be raised if the yaml contains a
b8524a
+  # class that isn't in the whitelist.
b8524a
+  #
b8524a
+  # A Psych::BadAlias exception will be raised if the yaml contains aliases
b8524a
+  # but the +aliases+ parameter is set to false.
b8524a
+  def self.safe_load yaml, whitelist_classes = [], whitelist_symbols = [], aliases = false, filename = nil
b8524a
+    result = parse(yaml, filename)
b8524a
+    return unless result
b8524a
+
b8524a
+    class_loader = ClassLoader::Restricted.new(whitelist_classes.map(&:to_s),
b8524a
+                                               whitelist_symbols.map(&:to_s))
b8524a
+    scanner      = ScalarScanner.new class_loader
b8524a
+    if aliases
b8524a
+      visitor = Visitors::ToRuby.new scanner, class_loader
b8524a
+    else
b8524a
+      visitor = Visitors::NoAliasRuby.new scanner, class_loader
b8524a
+    end
b8524a
+    visitor.accept result
b8524a
+  end
b8524a
+
b8524a
   ###
b8524a
   # Parse a YAML string in +yaml+.  Returns the first object of a YAML AST.
b8524a
   # +filename+ is used in the exception message if a Psych::SyntaxError is
b8524a
@@ -234,7 +283,7 @@ def self.dump o, io = nil, options = {}
b8524a
       io      = nil
b8524a
     end
b8524a
 
b8524a
-    visitor = Psych::Visitors::YAMLTree.new options
b8524a
+    visitor = Psych::Visitors::YAMLTree.create options
b8524a
     visitor << o
b8524a
     visitor.tree.yaml io, options
b8524a
   end
b8524a
@@ -246,7 +295,7 @@ def self.dump o, io = nil, options = {}
b8524a
   #
b8524a
   #   Psych.dump_stream("foo\n  ", {}) # => "--- ! \"foo\\n  \"\n--- {}\n"
b8524a
   def self.dump_stream *objects
b8524a
-    visitor = Psych::Visitors::YAMLTree.new {}
b8524a
+    visitor = Psych::Visitors::YAMLTree.create({})
b8524a
     objects.each do |o|
b8524a
       visitor << o
b8524a
     end
b8524a
@@ -256,7 +305,7 @@ def self.dump_stream *objects
b8524a
   ###
b8524a
   # Dump Ruby object +o+ to a JSON string.
b8524a
   def self.to_json o
b8524a
-    visitor = Psych::Visitors::JSONTree.new
b8524a
+    visitor = Psych::Visitors::JSONTree.create
b8524a
     visitor << o
b8524a
     visitor.tree.yaml
b8524a
   end
b8524a
@@ -314,7 +363,7 @@ def self.remove_type type_tag
b8524a
   @load_tags = {}
b8524a
   @dump_tags = {}
b8524a
   def self.add_tag tag, klass
b8524a
-    @load_tags[tag] = klass
b8524a
+    @load_tags[tag] = klass.name
b8524a
     @dump_tags[klass] = tag
b8524a
   end
b8524a
 
b8524a
diff --git a/ext/psych/lib/psych/class_loader.rb b/ext/psych/lib/psych/class_loader.rb
b8524a
new file mode 100644
b8524a
index 000000000000..46c6b9362790
b8524a
--- /dev/null
b8524a
+++ b/ext/psych/lib/psych/class_loader.rb
b8524a
@@ -0,0 +1,101 @@
b8524a
+require 'psych/omap'
b8524a
+require 'psych/set'
b8524a
+
b8524a
+module Psych
b8524a
+  class ClassLoader # :nodoc:
b8524a
+    BIG_DECIMAL = 'BigDecimal'
b8524a
+    COMPLEX     = 'Complex'
b8524a
+    DATE        = 'Date'
b8524a
+    DATE_TIME   = 'DateTime'
b8524a
+    EXCEPTION   = 'Exception'
b8524a
+    OBJECT      = 'Object'
b8524a
+    PSYCH_OMAP  = 'Psych::Omap'
b8524a
+    PSYCH_SET   = 'Psych::Set'
b8524a
+    RANGE       = 'Range'
b8524a
+    RATIONAL    = 'Rational'
b8524a
+    REGEXP      = 'Regexp'
b8524a
+    STRUCT      = 'Struct'
b8524a
+    SYMBOL      = 'Symbol'
b8524a
+
b8524a
+    def initialize
b8524a
+      @cache = CACHE.dup
b8524a
+    end
b8524a
+
b8524a
+    def load klassname
b8524a
+      return nil if !klassname || klassname.empty?
b8524a
+
b8524a
+      find klassname
b8524a
+    end
b8524a
+
b8524a
+    def symbolize sym
b8524a
+      symbol
b8524a
+      sym.to_sym
b8524a
+    end
b8524a
+
b8524a
+    constants.each do |const|
b8524a
+      konst = const_get const
b8524a
+      define_method(const.to_s.downcase) do
b8524a
+        load konst
b8524a
+      end
b8524a
+    end
b8524a
+
b8524a
+    private
b8524a
+
b8524a
+    def find klassname
b8524a
+      @cache[klassname] ||= resolve(klassname)
b8524a
+    end
b8524a
+
b8524a
+    def resolve klassname
b8524a
+      name    = klassname
b8524a
+      retried = false
b8524a
+
b8524a
+      begin
b8524a
+        path2class(name)
b8524a
+      rescue ArgumentError, NameError => ex
b8524a
+        unless retried
b8524a
+          name    = "Struct::#{name}"
b8524a
+          retried = ex
b8524a
+          retry
b8524a
+        end
b8524a
+        raise retried
b8524a
+      end
b8524a
+    end
b8524a
+
b8524a
+    CACHE = Hash[constants.map { |const|
b8524a
+      val = const_get const
b8524a
+      begin
b8524a
+        [val, ::Object.const_get(val)]
b8524a
+      rescue
b8524a
+        nil
b8524a
+      end
b8524a
+    }.compact]
b8524a
+
b8524a
+    class Restricted < ClassLoader
b8524a
+      def initialize classes, symbols
b8524a
+        @classes = classes
b8524a
+        @symbols = symbols
b8524a
+        super()
b8524a
+      end
b8524a
+
b8524a
+      def symbolize sym
b8524a
+        return super if @symbols.empty?
b8524a
+
b8524a
+        if @symbols.include? sym
b8524a
+          super
b8524a
+        else
b8524a
+          raise DisallowedClass, 'Symbol'
b8524a
+        end
b8524a
+      end
b8524a
+
b8524a
+      private
b8524a
+
b8524a
+      def find klassname
b8524a
+        if @classes.include? klassname
b8524a
+          super
b8524a
+        else
b8524a
+          raise DisallowedClass, klassname
b8524a
+        end
b8524a
+      end
b8524a
+    end
b8524a
+  end
b8524a
+end
b8524a
diff --git a/ext/psych/lib/psych/deprecated.rb b/ext/psych/lib/psych/deprecated.rb
b8524a
index 1e42859b22fe..8c310b320738 100644
b8524a
--- a/ext/psych/lib/psych/deprecated.rb
b8524a
+++ b/ext/psych/lib/psych/deprecated.rb
b8524a
@@ -35,7 +35,8 @@ def self.detect_implicit thing
b8524a
     warn "#{caller[0]}: detect_implicit is deprecated" if $VERBOSE
b8524a
     return '' unless String === thing
b8524a
     return 'null' if '' == thing
b8524a
-    ScalarScanner.new.tokenize(thing).class.name.downcase
b8524a
+    ss = ScalarScanner.new(ClassLoader.new)
b8524a
+    ss.tokenize(thing).class.name.downcase
b8524a
   end
b8524a
 
b8524a
   def self.add_ruby_type type_tag, &block
b8524a
diff --git a/ext/psych/lib/psych/exception.rb b/ext/psych/lib/psych/exception.rb
b8524a
index d96c527cfba7..ce9d2caf3fb2 100644
b8524a
--- a/ext/psych/lib/psych/exception.rb
b8524a
+++ b/ext/psych/lib/psych/exception.rb
b8524a
@@ -4,4 +4,10 @@ class Exception < RuntimeError
b8524a
 
b8524a
   class BadAlias < Exception
b8524a
   end
b8524a
+
b8524a
+  class DisallowedClass < Exception
b8524a
+    def initialize klass_name
b8524a
+      super "Tried to load unspecified class: #{klass_name}"
b8524a
+    end
b8524a
+  end
b8524a
 end
b8524a
diff --git a/ext/psych/lib/psych/json/stream.rb b/ext/psych/lib/psych/json/stream.rb
b8524a
index be1a0a8a8240..fe2a6e911650 100644
b8524a
--- a/ext/psych/lib/psych/json/stream.rb
b8524a
+++ b/ext/psych/lib/psych/json/stream.rb
b8524a
@@ -6,6 +6,7 @@ module JSON
b8524a
     class Stream < Psych::Visitors::JSONTree
b8524a
       include Psych::JSON::RubyEvents
b8524a
       include Psych::Streaming
b8524a
+      extend Psych::Streaming::ClassMethods
b8524a
 
b8524a
       class Emitter < Psych::Stream::Emitter # :nodoc:
b8524a
         include Psych::JSON::YAMLEvents
b8524a
diff --git a/ext/psych/lib/psych/nodes/node.rb b/ext/psych/lib/psych/nodes/node.rb
b8524a
index 0cefe44e446d..83233a61fdd3 100644
b8524a
--- a/ext/psych/lib/psych/nodes/node.rb
b8524a
+++ b/ext/psych/lib/psych/nodes/node.rb
b8524a
@@ -1,4 +1,6 @@
b8524a
 require 'stringio'
b8524a
+require 'psych/class_loader'
b8524a
+require 'psych/scalar_scanner'
b8524a
 
b8524a
 module Psych
b8524a
   module Nodes
b8524a
@@ -32,7 +34,7 @@ def each &block
b8524a
       #
b8524a
       # See also Psych::Visitors::ToRuby
b8524a
       def to_ruby
b8524a
-        Visitors::ToRuby.new.accept self
b8524a
+        Visitors::ToRuby.create.accept(self)
b8524a
       end
b8524a
       alias :transform :to_ruby
b8524a
 
b8524a
diff --git a/ext/psych/lib/psych/scalar_scanner.rb b/ext/psych/lib/psych/scalar_scanner.rb
b8524a
index 8aa594e3337c..5935e26b288a 100644
b8524a
--- a/ext/psych/lib/psych/scalar_scanner.rb
b8524a
+++ b/ext/psych/lib/psych/scalar_scanner.rb
b8524a
@@ -19,10 +19,13 @@ class ScalarScanner
b8524a
                   |[-+]?(?:0|[1-9][0-9_]*) (?# base 10)
b8524a
                   |[-+]?0x[0-9a-fA-F_]+    (?# base 16))$/x
b8524a
 
b8524a
+    attr_reader :class_loader
b8524a
+
b8524a
     # Create a new scanner
b8524a
-    def initialize
b8524a
+    def initialize class_loader
b8524a
       @string_cache = {}
b8524a
       @symbol_cache = {}
b8524a
+      @class_loader = class_loader
b8524a
     end
b8524a
 
b8524a
     # Tokenize +string+ returning the ruby object
b8524a
@@ -63,7 +66,7 @@ def tokenize string
b8524a
       when /^\d{4}-(?:1[012]|0\d|\d)-(?:[12]\d|3[01]|0\d|\d)$/
b8524a
         require 'date'
b8524a
         begin
b8524a
-          Date.strptime(string, '%Y-%m-%d')
b8524a
+          class_loader.date.strptime(string, '%Y-%m-%d')
b8524a
         rescue ArgumentError
b8524a
           string
b8524a
         end
b8524a
@@ -75,9 +78,9 @@ def tokenize string
b8524a
         Float::NAN
b8524a
       when /^:./
b8524a
         if string =~ /^:(["'])(.*)\1/
b8524a
-          @symbol_cache[string] = $2.sub(/^:/, '').to_sym
b8524a
+          @symbol_cache[string] = class_loader.symbolize($2.sub(/^:/, ''))
b8524a
         else
b8524a
-          @symbol_cache[string] = string.sub(/^:/, '').to_sym
b8524a
+          @symbol_cache[string] = class_loader.symbolize(string.sub(/^:/, ''))
b8524a
         end
b8524a
       when /^[-+]?[0-9][0-9_]*(:[0-5]?[0-9])+$/
b8524a
         i = 0
b8524a
@@ -117,6 +120,8 @@ def parse_int string
b8524a
     ###
b8524a
     # Parse and return a Time from +string+
b8524a
     def parse_time string
b8524a
+      klass = class_loader.load 'Time'
b8524a
+
b8524a
       date, time = *(string.split(/[ tT]/, 2))
b8524a
       (yy, m, dd) = date.split('-').map { |x| x.to_i }
b8524a
       md = time.match(/(\d+:\d+:\d+)(?:\.(\d*))?\s*(Z|[-+]\d+(:\d\d)?)?/)
b8524a
@@ -124,10 +129,10 @@ def parse_time string
b8524a
       (hh, mm, ss) = md[1].split(':').map { |x| x.to_i }
b8524a
       us = (md[2] ? Rational("0.#{md[2]}") : 0) * 1000000
b8524a
 
b8524a
-      time = Time.utc(yy, m, dd, hh, mm, ss, us)
b8524a
+      time = klass.utc(yy, m, dd, hh, mm, ss, us)
b8524a
 
b8524a
       return time if 'Z' == md[3]
b8524a
-      return Time.at(time.to_i, us) unless md[3]
b8524a
+      return klass.at(time.to_i, us) unless md[3]
b8524a
 
b8524a
       tz = md[3].match(/^([+\-]?\d{1,2})\:?(\d{1,2})?$/)[1..-1].compact.map { |digit| Integer(digit, 10) }
b8524a
       offset = tz.first * 3600
b8524a
@@ -138,7 +143,7 @@ def parse_time string
b8524a
         offset += ((tz[1] || 0) * 60)
b8524a
       end
b8524a
 
b8524a
-      Time.at((time - offset).to_i, us)
b8524a
+      klass.at((time - offset).to_i, us)
b8524a
     end
b8524a
   end
b8524a
 end
b8524a
diff --git a/ext/psych/lib/psych/stream.rb b/ext/psych/lib/psych/stream.rb
b8524a
index 567c1bb790f9..88c4c4cb4e18 100644
b8524a
--- a/ext/psych/lib/psych/stream.rb
b8524a
+++ b/ext/psych/lib/psych/stream.rb
b8524a
@@ -32,5 +32,6 @@ def streaming?
b8524a
     end
b8524a
 
b8524a
     include Psych::Streaming
b8524a
+    extend Psych::Streaming::ClassMethods
b8524a
   end
b8524a
 end
b8524a
diff --git a/ext/psych/lib/psych/streaming.rb b/ext/psych/lib/psych/streaming.rb
b8524a
index c6fa109d5a61..9d94eb549f26 100644
b8524a
--- a/ext/psych/lib/psych/streaming.rb
b8524a
+++ b/ext/psych/lib/psych/streaming.rb
b8524a
@@ -1,10 +1,15 @@
b8524a
 module Psych
b8524a
   module Streaming
b8524a
-    ###
b8524a
-    # Create a new streaming emitter.  Emitter will print to +io+.  See
b8524a
-    # Psych::Stream for an example.
b8524a
-    def initialize io
b8524a
-      super({}, self.class.const_get(:Emitter).new(io))
b8524a
+    module ClassMethods
b8524a
+      ###
b8524a
+      # Create a new streaming emitter.  Emitter will print to +io+.  See
b8524a
+      # Psych::Stream for an example.
b8524a
+      def new io
b8524a
+        emitter      = const_get(:Emitter).new(io)
b8524a
+        class_loader = ClassLoader.new
b8524a
+        ss           = ScalarScanner.new class_loader
b8524a
+        super(emitter, ss, {})
b8524a
+      end
b8524a
     end
b8524a
 
b8524a
     ###
b8524a
diff --git a/ext/psych/lib/psych/visitors/json_tree.rb b/ext/psych/lib/psych/visitors/json_tree.rb
b8524a
index 0350dd1faae0..0127ac8aa8c1 100644
b8524a
--- a/ext/psych/lib/psych/visitors/json_tree.rb
b8524a
+++ b/ext/psych/lib/psych/visitors/json_tree.rb
b8524a
@@ -5,8 +5,11 @@ module Visitors
b8524a
     class JSONTree < YAMLTree
b8524a
       include Psych::JSON::RubyEvents
b8524a
 
b8524a
-      def initialize options = {}, emitter = Psych::JSON::TreeBuilder.new
b8524a
-        super
b8524a
+      def self.create options = {}
b8524a
+        emitter = Psych::JSON::TreeBuilder.new
b8524a
+        class_loader = ClassLoader.new
b8524a
+        ss           = ScalarScanner.new class_loader
b8524a
+        new(emitter, ss, options)
b8524a
       end
b8524a
 
b8524a
       def accept target
b8524a
diff --git a/ext/psych/lib/psych/visitors/to_ruby.rb b/ext/psych/lib/psych/visitors/to_ruby.rb
b8524a
index 75c7bc0c550a..f770bb80aa3a 100644
b8524a
--- a/ext/psych/lib/psych/visitors/to_ruby.rb
b8524a
+++ b/ext/psych/lib/psych/visitors/to_ruby.rb
b8524a
@@ -1,4 +1,5 @@
b8524a
 require 'psych/scalar_scanner'
b8524a
+require 'psych/class_loader'
b8524a
 require 'psych/exception'
b8524a
 
b8524a
 unless defined?(Regexp::NOENCODING)
b8524a
@@ -10,11 +11,20 @@ module Visitors
b8524a
     ###
b8524a
     # This class walks a YAML AST, converting each node to ruby
b8524a
     class ToRuby < Psych::Visitors::Visitor
b8524a
-      def initialize ss = ScalarScanner.new
b8524a
+      def self.create
b8524a
+        class_loader = ClassLoader.new
b8524a
+        scanner      = ScalarScanner.new class_loader
b8524a
+        new(scanner, class_loader)
b8524a
+      end
b8524a
+
b8524a
+      attr_reader :class_loader
b8524a
+
b8524a
+      def initialize ss, class_loader
b8524a
         super()
b8524a
         @st = {}
b8524a
         @ss = ss
b8524a
         @domain_types = Psych.domain_types
b8524a
+        @class_loader = class_loader
b8524a
       end
b8524a
 
b8524a
       def accept target
b8524a
@@ -33,7 +43,7 @@ def accept target
b8524a
       end
b8524a
 
b8524a
       def deserialize o
b8524a
-        if klass = Psych.load_tags[o.tag]
b8524a
+        if klass = resolve_class(Psych.load_tags[o.tag])
b8524a
           instance = klass.allocate
b8524a
 
b8524a
           if instance.respond_to?(:init_with)
b8524a
@@ -60,19 +70,23 @@ def deserialize o
b8524a
           end
b8524a
         when '!ruby/object:BigDecimal'
b8524a
           require 'bigdecimal'
b8524a
-          BigDecimal._load o.value
b8524a
+          class_loader.big_decimal._load o.value
b8524a
         when "!ruby/object:DateTime"
b8524a
+          class_loader.date_time
b8524a
           require 'date'
b8524a
           @ss.parse_time(o.value).to_datetime
b8524a
         when "!ruby/object:Complex"
b8524a
+          class_loader.complex
b8524a
           Complex(o.value)
b8524a
         when "!ruby/object:Rational"
b8524a
+          class_loader.rational
b8524a
           Rational(o.value)
b8524a
         when "!ruby/class", "!ruby/module"
b8524a
           resolve_class o.value
b8524a
         when "tag:yaml.org,2002:float", "!float"
b8524a
           Float(@ss.tokenize(o.value))
b8524a
         when "!ruby/regexp"
b8524a
+          klass = class_loader.regexp
b8524a
           o.value =~ /^\/(.*)\/([mixn]*)$/
b8524a
           source  = $1
b8524a
           options = 0
b8524a
@@ -86,15 +100,16 @@ def deserialize o
b8524a
             else lang = option
b8524a
             end
b8524a
           end
b8524a
-          Regexp.new(*[source, options, lang].compact)
b8524a
+          klass.new(*[source, options, lang].compact)
b8524a
         when "!ruby/range"
b8524a
+          klass = class_loader.range
b8524a
           args = o.value.split(/([.]{2,3})/, 2).map { |s|
b8524a
             accept Nodes::Scalar.new(s)
b8524a
           }
b8524a
           args.push(args.delete_at(1) == '...')
b8524a
-          Range.new(*args)
b8524a
+          klass.new(*args)
b8524a
         when /^!ruby\/sym(bol)?:?(.*)?$/
b8524a
-          o.value.to_sym
b8524a
+          class_loader.symbolize o.value
b8524a
         else
b8524a
           @ss.tokenize o.value
b8524a
         end
b8524a
@@ -106,7 +121,7 @@ def visit_Psych_Nodes_Scalar o
b8524a
       end
b8524a
 
b8524a
       def visit_Psych_Nodes_Sequence o
b8524a
-        if klass = Psych.load_tags[o.tag]
b8524a
+        if klass = resolve_class(Psych.load_tags[o.tag])
b8524a
           instance = klass.allocate
b8524a
 
b8524a
           if instance.respond_to?(:init_with)
b8524a
@@ -138,22 +153,24 @@ def visit_Psych_Nodes_Sequence o
b8524a
       end
b8524a
 
b8524a
       def visit_Psych_Nodes_Mapping o
b8524a
-        return revive(Psych.load_tags[o.tag], o) if Psych.load_tags[o.tag]
b8524a
+        if Psych.load_tags[o.tag]
b8524a
+          return revive(resolve_class(Psych.load_tags[o.tag]), o)
b8524a
+        end
b8524a
         return revive_hash({}, o) unless o.tag
b8524a
 
b8524a
         case o.tag
b8524a
         when /^!ruby\/struct:?(.*)?$/
b8524a
-          klass = resolve_class($1)
b8524a
+          klass = resolve_class($1) if $1
b8524a
 
b8524a
           if klass
b8524a
             s = register(o, klass.allocate)
b8524a
 
b8524a
             members = {}
b8524a
-            struct_members = s.members.map { |x| x.to_sym }
b8524a
+            struct_members = s.members.map { |x| class_loader.symbolize x }
b8524a
             o.children.each_slice(2) do |k,v|
b8524a
               member = accept(k)
b8524a
               value  = accept(v)
b8524a
-              if struct_members.include?(member.to_sym)
b8524a
+              if struct_members.include?(class_loader.symbolize(member))
b8524a
                 s.send("#{member}=", value)
b8524a
               else
b8524a
                 members[member.to_s.sub(/^@/, '')] = value
b8524a
@@ -161,22 +178,27 @@ def visit_Psych_Nodes_Mapping o
b8524a
             end
b8524a
             init_with(s, members, o)
b8524a
           else
b8524a
+            klass = class_loader.struct
b8524a
             members = o.children.map { |c| accept c }
b8524a
             h = Hash[*members]
b8524a
-            Struct.new(*h.map { |k,v| k.to_sym }).new(*h.map { |k,v| v })
b8524a
+            klass.new(*h.map { |k,v|
b8524a
+              class_loader.symbolize k
b8524a
+            }).new(*h.map { |k,v| v })
b8524a
           end
b8524a
 
b8524a
         when /^!ruby\/object:?(.*)?$/
b8524a
           name = $1 || 'Object'
b8524a
 
b8524a
           if name == 'Complex'
b8524a
+            class_loader.complex
b8524a
             h = Hash[*o.children.map { |c| accept c }]
b8524a
             register o, Complex(h['real'], h['image'])
b8524a
           elsif name == 'Rational'
b8524a
+            class_loader.rational
b8524a
             h = Hash[*o.children.map { |c| accept c }]
b8524a
             register o, Rational(h['numerator'], h['denominator'])
b8524a
           else
b8524a
-            obj = revive((resolve_class(name) || Object), o)
b8524a
+            obj = revive((resolve_class(name) || class_loader.object), o)
b8524a
             obj
b8524a
           end
b8524a
 
b8524a
@@ -204,18 +226,19 @@ def visit_Psych_Nodes_Mapping o
b8524a
           list
b8524a
 
b8524a
         when '!ruby/range'
b8524a
+          klass = class_loader.range
b8524a
           h = Hash[*o.children.map { |c| accept c }]
b8524a
-          register o, Range.new(h['begin'], h['end'], h['excl'])
b8524a
+          register o, klass.new(h['begin'], h['end'], h['excl'])
b8524a
 
b8524a
         when /^!ruby\/exception:?(.*)?$/
b8524a
           h = Hash[*o.children.map { |c| accept c }]
b8524a
 
b8524a
-          e = build_exception((resolve_class($1) || Exception),
b8524a
+          e = build_exception((resolve_class($1) || class_loader.exception),
b8524a
                               h.delete('message'))
b8524a
           init_with(e, h, o)
b8524a
 
b8524a
         when '!set', 'tag:yaml.org,2002:set'
b8524a
-          set = Psych::Set.new
b8524a
+          set = class_loader.psych_set.new
b8524a
           @st[o.anchor] = set if o.anchor
b8524a
           o.children.each_slice(2) do |k,v|
b8524a
             set[accept(k)] = accept(v)
b8524a
@@ -226,7 +249,7 @@ def visit_Psych_Nodes_Mapping o
b8524a
           revive_hash resolve_class($1).new, o
b8524a
 
b8524a
         when '!omap', 'tag:yaml.org,2002:omap'
b8524a
-          map = register(o, Psych::Omap.new)
b8524a
+          map = register(o, class_loader.psych_omap.new)
b8524a
           o.children.each_slice(2) do |l,r|
b8524a
             map[accept(l)] = accept r
b8524a
           end
b8524a
@@ -326,21 +349,13 @@ def init_with o, h, node
b8524a
 
b8524a
       # Convert +klassname+ to a Class
b8524a
       def resolve_class klassname
b8524a
-        return nil unless klassname and not klassname.empty?
b8524a
-
b8524a
-        name    = klassname
b8524a
-        retried = false
b8524a
-
b8524a
-        begin
b8524a
-          path2class(name)
b8524a
-        rescue ArgumentError, NameError => ex
b8524a
-          unless retried
b8524a
-            name    = "Struct::#{name}"
b8524a
-            retried = ex
b8524a
-            retry
b8524a
-          end
b8524a
-          raise retried
b8524a
-        end
b8524a
+        class_loader.load klassname
b8524a
+      end
b8524a
+    end
b8524a
+
b8524a
+    class NoAliasRuby < ToRuby
b8524a
+      def visit_Psych_Nodes_Alias o
b8524a
+        raise BadAlias, "Unknown alias: #{o.anchor}"
b8524a
       end
b8524a
     end
b8524a
   end
b8524a
diff --git a/ext/psych/lib/psych/visitors/yaml_tree.rb b/ext/psych/lib/psych/visitors/yaml_tree.rb
b8524a
index 96640e026719..ddd745b34a9c 100644
b8524a
--- a/ext/psych/lib/psych/visitors/yaml_tree.rb
b8524a
+++ b/ext/psych/lib/psych/visitors/yaml_tree.rb
b8524a
@@ -1,3 +1,7 @@
b8524a
+require 'psych/tree_builder'
b8524a
+require 'psych/scalar_scanner'
b8524a
+require 'psych/class_loader'
b8524a
+
b8524a
 module Psych
b8524a
   module Visitors
b8524a
     ###
b8524a
@@ -36,7 +40,14 @@ def node_for target
b8524a
       alias :finished? :finished
b8524a
       alias :started? :started
b8524a
 
b8524a
-      def initialize options = {}, emitter = TreeBuilder.new, ss = ScalarScanner.new
b8524a
+      def self.create options = {}, emitter = nil
b8524a
+        emitter      ||= TreeBuilder.new
b8524a
+        class_loader = ClassLoader.new
b8524a
+        ss           = ScalarScanner.new class_loader
b8524a
+        new(emitter, ss, options)
b8524a
+      end
b8524a
+
b8524a
+      def initialize emitter, ss, options
b8524a
         super()
b8524a
         @started  = false
b8524a
         @finished = false
b8524a
diff --git a/ext/psych/psych_to_ruby.c b/ext/psych/psych_to_ruby.c
b8524a
index ed5245e12e7a..3cc87a965ec1 100644
b8524a
--- a/ext/psych/psych_to_ruby.c
b8524a
+++ b/ext/psych/psych_to_ruby.c
b8524a
@@ -31,11 +31,13 @@ static VALUE path2class(VALUE self, VALUE path)
b8524a
 void Init_psych_to_ruby(void)
b8524a
 {
b8524a
     VALUE psych     = rb_define_module("Psych");
b8524a
+    VALUE class_loader  = rb_define_class_under(psych, "ClassLoader", rb_cObject);
b8524a
+
b8524a
     VALUE visitors  = rb_define_module_under(psych, "Visitors");
b8524a
     VALUE visitor   = rb_define_class_under(visitors, "Visitor", rb_cObject);
b8524a
     cPsychVisitorsToRuby = rb_define_class_under(visitors, "ToRuby", visitor);
b8524a
 
b8524a
     rb_define_private_method(cPsychVisitorsToRuby, "build_exception", build_exception, 2);
b8524a
-    rb_define_private_method(cPsychVisitorsToRuby, "path2class", path2class, 1);
b8524a
+    rb_define_private_method(class_loader, "path2class", path2class, 1);
b8524a
 }
b8524a
 /* vim: set noet sws=4 sw=4: */
b8524a
diff --git a/test/psych/helper.rb b/test/psych/helper.rb
b8524a
index 77ab0bb9d71c..f9b73cf5b588 100644
b8524a
--- a/test/psych/helper.rb
b8524a
+++ b/test/psych/helper.rb
b8524a
@@ -31,7 +31,7 @@ def assert_parse_only( obj, yaml )
b8524a
     end
b8524a
 
b8524a
     def assert_cycle( obj )
b8524a
-      v = Visitors::YAMLTree.new
b8524a
+      v = Visitors::YAMLTree.create
b8524a
       v << obj
b8524a
       assert_equal(obj, Psych.load(v.tree.yaml))
b8524a
       assert_equal( obj, Psych::load(Psych.dump(obj)))
b8524a
diff --git a/test/psych/test_safe_load.rb b/test/psych/test_safe_load.rb
b8524a
new file mode 100644
b8524a
index 000000000000..dd299c0ebf40
b8524a
--- /dev/null
b8524a
+++ b/test/psych/test_safe_load.rb
b8524a
@@ -0,0 +1,97 @@
b8524a
+require 'psych/helper'
b8524a
+
b8524a
+module Psych
b8524a
+  class TestSafeLoad < TestCase
b8524a
+    class Foo; end
b8524a
+
b8524a
+    [1, 2.2, {}, [], "foo"].each do |obj|
b8524a
+      define_method(:"test_basic_#{obj.class}") do
b8524a
+        assert_safe_cycle obj
b8524a
+      end
b8524a
+    end
b8524a
+
b8524a
+    def test_no_recursion
b8524a
+      x = []
b8524a
+      x << x
b8524a
+      assert_raises(Psych::BadAlias) do
b8524a
+        Psych.safe_load Psych.dump(x)
b8524a
+      end
b8524a
+    end
b8524a
+
b8524a
+    def test_explicit_recursion
b8524a
+      x = []
b8524a
+      x << x
b8524a
+      assert_equal(x, Psych.safe_load(Psych.dump(x), [], [], true))
b8524a
+    end
b8524a
+
b8524a
+    def test_symbol_whitelist
b8524a
+      yml = Psych.dump :foo
b8524a
+      assert_raises(Psych::DisallowedClass) do
b8524a
+        Psych.safe_load yml
b8524a
+      end
b8524a
+      assert_equal(:foo, Psych.safe_load(yml, [Symbol], [:foo]))
b8524a
+    end
b8524a
+
b8524a
+    def test_symbol
b8524a
+      assert_raises(Psych::DisallowedClass) do
b8524a
+        assert_safe_cycle :foo
b8524a
+      end
b8524a
+      assert_raises(Psych::DisallowedClass) do
b8524a
+        Psych.safe_load '--- !ruby/symbol foo', []
b8524a
+      end
b8524a
+      assert_safe_cycle :foo, [Symbol]
b8524a
+      assert_safe_cycle :foo, %w{ Symbol }
b8524a
+      assert_equal :foo, Psych.safe_load('--- !ruby/symbol foo', [Symbol])
b8524a
+    end
b8524a
+
b8524a
+    def test_foo
b8524a
+      assert_raises(Psych::DisallowedClass) do
b8524a
+        Psych.safe_load '--- !ruby/object:Foo {}', [Foo]
b8524a
+      end
b8524a
+      assert_raises(Psych::DisallowedClass) do
b8524a
+        assert_safe_cycle Foo.new
b8524a
+      end
b8524a
+      assert_kind_of(Foo, Psych.safe_load(Psych.dump(Foo.new), [Foo]))
b8524a
+    end
b8524a
+
b8524a
+    X = Struct.new(:x)
b8524a
+    def test_struct_depends_on_sym
b8524a
+      assert_safe_cycle(X.new, [X, Symbol])
b8524a
+      assert_raises(Psych::DisallowedClass) do
b8524a
+        cycle X.new, [X]
b8524a
+      end
b8524a
+    end
b8524a
+
b8524a
+    def test_anon_struct
b8524a
+      assert Psych.safe_load(<<-eoyml, [Struct, Symbol])
b8524a
+--- !ruby/struct
b8524a
+  foo: bar
b8524a
+                      eoyml
b8524a
+
b8524a
+      assert_raises(Psych::DisallowedClass) do
b8524a
+        Psych.safe_load(<<-eoyml, [Struct])
b8524a
+--- !ruby/struct
b8524a
+  foo: bar
b8524a
+                      eoyml
b8524a
+      end
b8524a
+
b8524a
+      assert_raises(Psych::DisallowedClass) do
b8524a
+        Psych.safe_load(<<-eoyml, [Symbol])
b8524a
+--- !ruby/struct
b8524a
+  foo: bar
b8524a
+                      eoyml
b8524a
+      end
b8524a
+    end
b8524a
+
b8524a
+    private
b8524a
+
b8524a
+    def cycle object, whitelist = []
b8524a
+      Psych.safe_load(Psych.dump(object), whitelist)
b8524a
+    end
b8524a
+
b8524a
+    def assert_safe_cycle object, whitelist = []
b8524a
+      other = cycle object, whitelist
b8524a
+      assert_equal object, other
b8524a
+    end
b8524a
+  end
b8524a
+end
b8524a
diff --git a/test/psych/test_scalar_scanner.rb b/test/psych/test_scalar_scanner.rb
b8524a
index a7bf17c912b6..e8e423cb053d 100644
b8524a
--- a/test/psych/test_scalar_scanner.rb
b8524a
+++ b/test/psych/test_scalar_scanner.rb
b8524a
@@ -7,7 +7,7 @@ class TestScalarScanner < TestCase
b8524a
 
b8524a
     def setup
b8524a
       super
b8524a
-      @ss = Psych::ScalarScanner.new
b8524a
+      @ss = Psych::ScalarScanner.new ClassLoader.new
b8524a
     end
b8524a
 
b8524a
     def test_scan_time
b8524a
diff --git a/test/psych/visitors/test_to_ruby.rb b/test/psych/visitors/test_to_ruby.rb
b8524a
index 022cc2d2d4ea..c13d980468d4 100644
b8524a
--- a/test/psych/visitors/test_to_ruby.rb
b8524a
+++ b/test/psych/visitors/test_to_ruby.rb
b8524a
@@ -6,7 +6,7 @@ module Visitors
b8524a
     class TestToRuby < TestCase
b8524a
       def setup
b8524a
         super
b8524a
-        @visitor = ToRuby.new
b8524a
+        @visitor = ToRuby.create
b8524a
       end
b8524a
 
b8524a
       def test_object
b8524a
@@ -88,7 +88,7 @@ def test_anon_struct
b8524a
       end
b8524a
 
b8524a
       def test_exception
b8524a
-        exc = Exception.new 'hello'
b8524a
+        exc = ::Exception.new 'hello'
b8524a
 
b8524a
         mapping = Nodes::Mapping.new nil, '!ruby/exception'
b8524a
         mapping.children << Nodes::Scalar.new('message')
b8524a
diff --git a/test/psych/visitors/test_yaml_tree.rb b/test/psych/visitors/test_yaml_tree.rb
b8524a
index 496cdd05cc34..40702bce796f 100644
b8524a
--- a/test/psych/visitors/test_yaml_tree.rb
b8524a
+++ b/test/psych/visitors/test_yaml_tree.rb
b8524a
@@ -5,7 +5,7 @@ module Visitors
b8524a
     class TestYAMLTree < TestCase
b8524a
       def setup
b8524a
         super
b8524a
-        @v = Visitors::YAMLTree.new
b8524a
+        @v = Visitors::YAMLTree.create
b8524a
       end
b8524a
 
b8524a
       def test_tree_can_be_called_twice
b8524a
@@ -18,7 +18,7 @@ def test_tree_can_be_called_twice
b8524a
       def test_yaml_tree_can_take_an_emitter
b8524a
         io = StringIO.new
b8524a
         e  = Psych::Emitter.new io
b8524a
-        v = Visitors::YAMLTree.new({}, e)
b8524a
+        v = Visitors::YAMLTree.create({}, e)
b8524a
         v.start
b8524a
         v << "hello world"
b8524a
         v.finish