Skip to content

Commit

Permalink
* ext/psych/lib/psych.rb: Adding Psych.safe_load for loading a user
Browse files Browse the repository at this point in the history
  defined, restricted subset of Ruby object types.
* ext/psych/lib/psych/class_loader.rb: A class loader for
  encapsulating the logic for which objects are allowed to be
  deserialized.
* ext/psych/lib/psych/deprecated.rb: Changes to use the class loader
* ext/psych/lib/psych/exception.rb: ditto
* ext/psych/lib/psych/json/stream.rb: ditto
* ext/psych/lib/psych/nodes/node.rb: ditto
* ext/psych/lib/psych/scalar_scanner.rb: ditto
* ext/psych/lib/psych/stream.rb: ditto
* ext/psych/lib/psych/streaming.rb: ditto
* ext/psych/lib/psych/visitors/json_tree.rb: ditto
* ext/psych/lib/psych/visitors/to_ruby.rb: ditto
* ext/psych/lib/psych/visitors/yaml_tree.rb: ditto
* ext/psych/psych_to_ruby.c: ditto
* test/psych/helper.rb: ditto
* test/psych/test_safe_load.rb: tests for restricted subset.
* test/psych/test_scalar_scanner.rb: ditto
* test/psych/visitors/test_to_ruby.rb: ditto
* test/psych/visitors/test_yaml_tree.rb: ditto
  • Loading branch information
tenderlove committed May 14, 2013
1 parent d73609e commit 2c644e1
Show file tree
Hide file tree
Showing 19 changed files with 383 additions and 60 deletions.
24 changes: 24 additions & 0 deletions CHANGELOG.rdoc
Original file line number Diff line number Diff line change
@@ -1,3 +1,27 @@
Wed May 15 02:22:16 2013 Aaron Patterson <aaron@tenderlovemaking.com>

* ext/psych/lib/psych.rb: Adding Psych.safe_load for loading a user
defined, restricted subset of Ruby object types.
* ext/psych/lib/psych/class_loader.rb: A class loader for
encapsulating the logic for which objects are allowed to be
deserialized.
* ext/psych/lib/psych/deprecated.rb: Changes to use the class loader
* ext/psych/lib/psych/exception.rb: ditto
* ext/psych/lib/psych/json/stream.rb: ditto
* ext/psych/lib/psych/nodes/node.rb: ditto
* ext/psych/lib/psych/scalar_scanner.rb: ditto
* ext/psych/lib/psych/stream.rb: ditto
* ext/psych/lib/psych/streaming.rb: ditto
* ext/psych/lib/psych/visitors/json_tree.rb: ditto
* ext/psych/lib/psych/visitors/to_ruby.rb: ditto
* ext/psych/lib/psych/visitors/yaml_tree.rb: ditto
* ext/psych/psych_to_ruby.c: ditto
* test/psych/helper.rb: ditto
* test/psych/test_safe_load.rb: tests for restricted subset.
* test/psych/test_scalar_scanner.rb: ditto
* test/psych/visitors/test_to_ruby.rb: ditto
* test/psych/visitors/test_yaml_tree.rb: ditto

Sat Apr 6 02:54:08 2013 Aaron Patterson <aaron@tenderlovemaking.com>

* ext/psych/lib/psych/exception.rb: there should be only one exception
Expand Down
4 changes: 3 additions & 1 deletion ext/psych/psych_to_ruby.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,13 @@ static VALUE path2class(VALUE self, VALUE path)
void Init_psych_to_ruby(void)
{
VALUE psych = rb_define_module("Psych");
VALUE class_loader = rb_define_class_under(psych, "ClassLoader", rb_cObject);

VALUE visitors = rb_define_module_under(psych, "Visitors");
VALUE visitor = rb_define_class_under(visitors, "Visitor", rb_cObject);
cPsychVisitorsToRuby = rb_define_class_under(visitors, "ToRuby", visitor);

rb_define_private_method(cPsychVisitorsToRuby, "build_exception", build_exception, 2);
rb_define_private_method(cPsychVisitorsToRuby, "path2class", path2class, 1);
rb_define_private_method(class_loader, "path2class", path2class, 1);
}
/* vim: set noet sws=4 sw=4: */
57 changes: 53 additions & 4 deletions lib/psych.rb
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,55 @@ def self.load yaml, filename = nil
result ? result.to_ruby : result
end

###
# Safely load the yaml string in +yaml+. By default, only the following
# classes are allowed to be deserialized:
#
# * TrueClass
# * FalseClass
# * NilClass
# * Numeric
# * String
# * Array
# * Hash
#
# Recursive data structures are not allowed by default. Arbitrary classes
# can be allowed by adding those classes to the +whitelist+. They are
# additive. For example, to allow Date deserialization:
#
# Psych.safe_load(yaml, [Date])
#
# Now the Date class can be loaded in addition to the classes listed above.
#
# Aliases can be explicitly allowed by changing the +aliases+ parameter.
# For example:
#
# x = []
# x << x
# yaml = Psych.dump x
# Psych.safe_load yaml # => raises an exception
# Psych.safe_load yaml, [], [], true # => loads the aliases
#
# A Psych::DisallowedClass exception will be raised if the yaml contains a
# class that isn't in the whitelist.
#
# A Psych::BadAlias exception will be raised if the yaml contains aliases
# but the +aliases+ parameter is set to false.
def self.safe_load yaml, whitelist_classes = [], whitelist_symbols = [], aliases = false, filename = nil
result = parse(yaml, filename)
return unless result

class_loader = ClassLoader::Restricted.new(whitelist_classes.map(&:to_s),
whitelist_symbols.map(&:to_s))
scanner = ScalarScanner.new class_loader
if aliases
visitor = Visitors::ToRuby.new scanner, class_loader
else
visitor = Visitors::NoAliasRuby.new scanner, class_loader
end
visitor.accept result
end

###
# Parse a YAML string in +yaml+. Returns the Psych::Nodes::Document.
# +filename+ is used in the exception message if a Psych::SyntaxError is
Expand Down Expand Up @@ -355,7 +404,7 @@ def self.dump o, io = nil, options = {}
io = nil
end

visitor = Psych::Visitors::YAMLTree.new options
visitor = Psych::Visitors::YAMLTree.create options
visitor << o
visitor.tree.yaml io, options
end
Expand All @@ -367,7 +416,7 @@ def self.dump o, io = nil, options = {}
#
# Psych.dump_stream("foo\n ", {}) # => "--- ! \"foo\\n \"\n--- {}\n"
def self.dump_stream *objects
visitor = Psych::Visitors::YAMLTree.new {}
visitor = Psych::Visitors::YAMLTree.create({})
objects.each do |o|
visitor << o
end
Expand All @@ -377,7 +426,7 @@ def self.dump_stream *objects
###
# Dump Ruby +object+ to a JSON string.
def self.to_json object
visitor = Psych::Visitors::JSONTree.new
visitor = Psych::Visitors::JSONTree.create
visitor << object
visitor.tree.yaml
end
Expand Down Expand Up @@ -435,7 +484,7 @@ def self.remove_type type_tag
@load_tags = {}
@dump_tags = {}
def self.add_tag tag, klass
@load_tags[tag] = klass
@load_tags[tag] = klass.name
@dump_tags[klass] = tag
end

Expand Down
101 changes: 101 additions & 0 deletions lib/psych/class_loader.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
require 'psych/omap'
require 'psych/set'

module Psych
class ClassLoader # :nodoc:
BIG_DECIMAL = 'BigDecimal'
COMPLEX = 'Complex'
DATE = 'Date'
DATE_TIME = 'DateTime'
EXCEPTION = 'Exception'
OBJECT = 'Object'
PSYCH_OMAP = 'Psych::Omap'
PSYCH_SET = 'Psych::Set'
RANGE = 'Range'
RATIONAL = 'Rational'
REGEXP = 'Regexp'
STRUCT = 'Struct'
SYMBOL = 'Symbol'

def initialize
@cache = CACHE.dup
end

def load klassname
return nil if !klassname || klassname.empty?

find klassname
end

def symbolize sym
symbol
sym.to_sym
end

constants.each do |const|
konst = const_get const
define_method(const.to_s.downcase) do
load konst
end
end

private

def find klassname
@cache[klassname] ||= resolve(klassname)
end

def resolve klassname
name = klassname
retried = false

begin
path2class(name)
rescue ArgumentError, NameError => ex
unless retried
name = "Struct::#{name}"
retried = ex
retry
end
raise retried
end
end

CACHE = Hash[constants.map { |const|
val = const_get const
begin
[val, ::Object.const_get(val)]
rescue
nil
end
}.compact]

class Restricted < ClassLoader
def initialize classes, symbols
@classes = classes
@symbols = symbols
super()
end

def symbolize sym
return super if @symbols.empty?

if @symbols.include? sym
super
else
raise DisallowedClass, 'Symbol'
end
end

private

def find klassname
if @classes.include? klassname
super
else
raise DisallowedClass, klassname
end
end
end
end
end
3 changes: 2 additions & 1 deletion lib/psych/deprecated.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ def self.detect_implicit thing
warn "#{caller[0]}: detect_implicit is deprecated" if $VERBOSE
return '' unless String === thing
return 'null' if '' == thing
ScalarScanner.new.tokenize(thing).class.name.downcase
ss = ScalarScanner.new(ClassLoader.new)
ss.tokenize(thing).class.name.downcase
end

def self.add_ruby_type type_tag, &block
Expand Down
6 changes: 6 additions & 0 deletions lib/psych/exception.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,10 @@ class Exception < RuntimeError

class BadAlias < Exception
end

class DisallowedClass < Exception
def initialize klass_name
super "Tried to load unspecified class: #{klass_name}"
end
end
end
1 change: 1 addition & 0 deletions lib/psych/json/stream.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module JSON
class Stream < Psych::Visitors::JSONTree
include Psych::JSON::RubyEvents
include Psych::Streaming
extend Psych::Streaming::ClassMethods

class Emitter < Psych::Stream::Emitter # :nodoc:
include Psych::JSON::YAMLEvents
Expand Down
4 changes: 3 additions & 1 deletion lib/psych/nodes/node.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
require 'stringio'
require 'psych/class_loader'
require 'psych/scalar_scanner'

module Psych
module Nodes
Expand Down Expand Up @@ -32,7 +34,7 @@ def each &block
#
# See also Psych::Visitors::ToRuby
def to_ruby
Visitors::ToRuby.new.accept self
Visitors::ToRuby.create.accept(self)
end
alias :transform :to_ruby

Expand Down
19 changes: 12 additions & 7 deletions lib/psych/scalar_scanner.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ class ScalarScanner
|[-+]?(?:0|[1-9][0-9_]*) (?# base 10)
|[-+]?0x[0-9a-fA-F_]+ (?# base 16))$/x

attr_reader :class_loader

# Create a new scanner
def initialize
def initialize class_loader
@string_cache = {}
@symbol_cache = {}
@class_loader = class_loader
end

# Tokenize +string+ returning the ruby object
Expand Down Expand Up @@ -63,7 +66,7 @@ def tokenize string
when /^\d{4}-(?:1[012]|0\d|\d)-(?:[12]\d|3[01]|0\d|\d)$/
require 'date'
begin
Date.strptime(string, '%Y-%m-%d')
class_loader.date.strptime(string, '%Y-%m-%d')
rescue ArgumentError
string
end
Expand All @@ -75,9 +78,9 @@ def tokenize string
Float::NAN
when /^:./
if string =~ /^:(["'])(.*)\1/
@symbol_cache[string] = $2.sub(/^:/, '').to_sym
@symbol_cache[string] = class_loader.symbolize($2.sub(/^:/, ''))
else
@symbol_cache[string] = string.sub(/^:/, '').to_sym
@symbol_cache[string] = class_loader.symbolize(string.sub(/^:/, ''))
end
when /^[-+]?[0-9][0-9_]*(:[0-5]?[0-9])+$/
i = 0
Expand Down Expand Up @@ -117,17 +120,19 @@ def parse_int string
###
# Parse and return a Time from +string+
def parse_time string
klass = class_loader.load 'Time'

date, time = *(string.split(/[ tT]/, 2))
(yy, m, dd) = date.split('-').map { |x| x.to_i }
md = time.match(/(\d+:\d+:\d+)(?:\.(\d*))?\s*(Z|[-+]\d+(:\d\d)?)?/)

(hh, mm, ss) = md[1].split(':').map { |x| x.to_i }
us = (md[2] ? Rational("0.#{md[2]}") : 0) * 1000000

time = Time.utc(yy, m, dd, hh, mm, ss, us)
time = klass.utc(yy, m, dd, hh, mm, ss, us)

return time if 'Z' == md[3]
return Time.at(time.to_i, us) unless md[3]
return klass.at(time.to_i, us) unless md[3]

tz = md[3].match(/^([+\-]?\d{1,2})\:?(\d{1,2})?$/)[1..-1].compact.map { |digit| Integer(digit, 10) }
offset = tz.first * 3600
Expand All @@ -138,7 +143,7 @@ def parse_time string
offset += ((tz[1] || 0) * 60)
end

Time.at((time - offset).to_i, us)
klass.at((time - offset).to_i, us)
end
end
end
1 change: 1 addition & 0 deletions lib/psych/stream.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,5 +32,6 @@ def streaming?
end

include Psych::Streaming
extend Psych::Streaming::ClassMethods
end
end
15 changes: 10 additions & 5 deletions lib/psych/streaming.rb
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
module Psych
module Streaming
###
# Create a new streaming emitter. Emitter will print to +io+. See
# Psych::Stream for an example.
def initialize io
super({}, self.class.const_get(:Emitter).new(io))
module ClassMethods
###
# Create a new streaming emitter. Emitter will print to +io+. See
# Psych::Stream for an example.
def new io
emitter = const_get(:Emitter).new(io)
class_loader = ClassLoader.new
ss = ScalarScanner.new class_loader
super(emitter, ss, {})
end
end

###
Expand Down
7 changes: 5 additions & 2 deletions lib/psych/visitors/json_tree.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ module Visitors
class JSONTree < YAMLTree
include Psych::JSON::RubyEvents

def initialize options = {}, emitter = Psych::JSON::TreeBuilder.new
super
def self.create options = {}
emitter = Psych::JSON::TreeBuilder.new
class_loader = ClassLoader.new
ss = ScalarScanner.new class_loader
new(emitter, ss, options)
end

def accept target
Expand Down
Loading

0 comments on commit 2c644e1

Please sign in to comment.