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