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