safemode 0.0.2

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of safemode might be problematic. Click here for more details.

data/LICENCSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2008, Sven Fuchs, Peter Cooper
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without
6
+ restriction, including without limitation the rights to use,
7
+ copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ copies of the Software, and to permit persons to whom the
9
+ Software is furnished to do so, subject to the following
10
+ conditions:
11
+
12
+ The above copyright notice and this permission notice shall be
13
+ included in all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
+ HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
+ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
+ OTHER DEALINGS IN THE SOFTWARE.
data/README.markdown ADDED
@@ -0,0 +1,71 @@
1
+ ## Safemode
2
+
3
+ A library for safe evaluation of Ruby code based on RubyParser and
4
+ Ruby2Ruby. Provides Rails ActionView template handlers for ERB and Haml.
5
+
6
+ ### Word of warning
7
+
8
+ This library is still highly experimental. Only use it at your own risk for
9
+ anything beyond experiments and playing.
10
+
11
+ That said, please **do** play with it, read and run the unit tests and provide
12
+ feedback to help make it waterproof and finally suitable for serious purposes.
13
+
14
+ ### Usage
15
+
16
+ For manual evaluation of Ruby code and ERB templates see demo.rb
17
+
18
+ You can use the ActionView template handlers by registering them, e.g., in
19
+ a config/initializer file like this:
20
+
21
+ # in config/intializer/safemode_tempate_handlers.rb
22
+ ActionView::Template.register_template_handler :serb, ActionView::TemplateHandlers::SafeErb
23
+ ActionView::Template.register_template_handler :haml, ActionView::TemplateHandlers::SafeHaml
24
+
25
+ If you register the ERB template handler for the file extension :erb be aware
26
+ that this most probably will break when your application tries to render an
27
+ error message in development mode (because Rails will try to use the handler
28
+ to render the error message itself).
29
+
30
+ You will then have to "whitelist" all method calls to the objects that are
31
+ registered as template variables by explicitely allowing access to them. You
32
+ can do that by defining a Safemode::Jail class for your classes, like so:
33
+
34
+ class User
35
+ class Jail < Safemode::Jail
36
+ allow :name
37
+ end
38
+ end
39
+
40
+ This will allow your template users to access the name method on your User
41
+ objects.
42
+
43
+ For more details about the concepts behind Safemode please refer to the
44
+ following blog posts until a more comprehensive writeup is available:
45
+
46
+ * Initial reasoning: [http://www.artweb-design.de/2008/2/5/sexy-theme-templating-with-haml-safemode-finally](http://www.artweb-design.de/2008/2/5/sexy-theme-templating-with-haml-safemode-finally)
47
+ * Refined concept: [http://www.artweb-design.de/2008/2/17/sending-ruby-to-the-jail-an-attemp-on-a-haml-safemode](http://www.artweb-design.de/2008/2/17/sending-ruby-to-the-jail-an-attemp-on-a-haml-safemode)
48
+ * ActionView ERB handler: [http://www.artweb-design.de/2008/4/22/an-erb-safemode-handler-for-actionview](http://www.artweb-design.de/2008/4/22/an-erb-safemode-handler-for-actionview)
49
+
50
+ ### Dependencies
51
+
52
+ Requires the gems:
53
+
54
+ * RubyParser
55
+ * Ruby2Ruby
56
+
57
+ As of writing RubyParser alters StringIO and thus breaks usage with Rails.
58
+ See [http://www.zenspider.com/pipermail/parsetree/2008-April/000026.html](http://www.zenspider.com/pipermail/parsetree/2008-April/000026.html)
59
+
60
+ A patch is included that fixes this issue and can be applied to RubyParser.
61
+ See lib/ruby\_parser\_string\_io\_patch.diff
62
+
63
+ ### Credits
64
+
65
+ * Sven Fuchs - Maintainer
66
+ * Peter Cooper
67
+
68
+ This code and all of the Safemode library's code was initially written by
69
+ Sven Fuchs to allow Haml to have a safe mode. It was then modified and
70
+ re-structured by Peter Cooper and Sven Fuchs to extend the idea to generic
71
+ Ruby eval situations.
data/Rakefile ADDED
@@ -0,0 +1,15 @@
1
+ # optional libraries
2
+ %w[ redgreen ].each do |lib|
3
+ begin
4
+ require lib
5
+ rescue LoadError
6
+ end
7
+ end
8
+
9
+ task :default => [:test]
10
+
11
+ task :test do
12
+ ['test/unit', 'test/test_helper', 'test/test_all'].each do |file|
13
+ require file
14
+ end
15
+ end
data/demo.rb ADDED
@@ -0,0 +1,23 @@
1
+ require 'safemode'
2
+ require 'erb'
3
+
4
+ erb_code = %q{<% 10.times do |i| %><%= i %><% end %>}
5
+
6
+ raw_code = %q{
7
+ (1..10).to_a.collect do |i|
8
+ puts i
9
+ i * 2
10
+ end.join(', ')
11
+ }
12
+
13
+ box = Safemode::Box.new
14
+
15
+ puts "Doing the ERB code in safe mode\n-----"
16
+ puts box.eval(ERB.new(erb_code).src)
17
+
18
+ puts "\nDoing the regular Ruby code in safe mode\n-----"
19
+ puts box.eval(raw_code)
20
+
21
+ puts "\nOutput from regular Ruby code\n-----"
22
+ puts box.output
23
+
data/init.rb ADDED
@@ -0,0 +1 @@
1
+ require 'safemode'
@@ -0,0 +1,43 @@
1
+ require 'safemode'
2
+ require 'erb'
3
+
4
+ module ActionView
5
+ module TemplateHandlers
6
+ class SafeErb < TemplateHandler
7
+ include Compilable rescue nil # does not exist prior Rails 2.1
8
+ extend SafemodeHandler
9
+
10
+ def self.line_offset
11
+ 0
12
+ end
13
+
14
+ def compile(template)
15
+ # Rails 2.0 passes the template source, while Rails 2.1 passes the
16
+ # template instance
17
+ src = template.respond_to?(:source) ? template.source : template
18
+ filename = template.filename rescue nil
19
+ erb_trim_mode = '-'
20
+
21
+ # code = ::ERB.new(src, nil, @view.erb_trim_mode).src
22
+ code = ::ERB.new("<% __in_erb_template=true %>#{src}", nil, erb_trim_mode, '@output_buffer').src
23
+ # Ruby 1.9 prepends an encoding to the source. However this is
24
+ # useless because you can only set an encoding on the first line
25
+ RUBY_VERSION >= '1.9' ? src.sub(/\A#coding:.*\n/, '') : src
26
+
27
+ code.gsub!('\\','\\\\\\') # backslashes would disappear in compile_template/modul_eval, so we escape them
28
+
29
+ code = <<-CODE
30
+ handler = ActionView::TemplateHandlers::SafeHaml
31
+ assigns = handler.valid_assigns(@template.assigns)
32
+ methods = handler.delegate_methods(self)
33
+ code = %Q(#{code});
34
+
35
+ box = Safemode::Box.new(self, methods, #{filename.inspect}, 0)
36
+ box.eval(code, assigns, local_assigns, &lambda{ |*args| yield(*args) })
37
+ CODE
38
+ # puts code
39
+ code
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,27 @@
1
+ require 'safemode'
2
+ require 'haml/safemode'
3
+
4
+ module ActionView
5
+ module TemplateHandlers
6
+ class SafeHaml < TemplateHandler
7
+ include Compilable rescue nil # does not exist prior Rails 2.1
8
+ extend SafemodeHandler
9
+
10
+ def self.line_offset
11
+ 3
12
+ end
13
+
14
+ def compile(template)
15
+ # Rails 2.0 passes the template source, while Rails 2.1 passes the
16
+ # template instance
17
+ src = template.respond_to?(:source) ? template.source : template
18
+ filename = template.filename rescue nil
19
+
20
+ options = Haml::Template.options.dup
21
+ haml = Haml::Engine.new template, options
22
+ methods = delegate_methods + ActionController::Routing::Routes.named_routes.helpers
23
+ haml.precompile_for_safemode filename, ignore_assigns, methods
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ module ActionView
2
+ module TemplateHandlers
3
+ module SafemodeHandler
4
+
5
+ def valid_assigns(assigns)
6
+ assigns = assigns.reject{|key, value| skip_assigns.include?(key) }
7
+ end
8
+
9
+ def delegate_methods(view)
10
+ [ :render, :params, :flash ] +
11
+ helper_methods(view) +
12
+ ActionController::Routing::Routes.named_routes.helpers
13
+ end
14
+
15
+ def helper_methods(view)
16
+ view.class.included_modules.collect {|m| m.instance_methods(false) }.flatten.map(&:to_sym)
17
+ end
18
+
19
+ def skip_assigns
20
+ [ "_cookies", "_flash", "_headers", "_params", "_request",
21
+ "_response", "_session", "before_filter_chain_aborted",
22
+ "ignore_missing_templates", "logger", "request_origin",
23
+ "template", "template_class", "url", "variables_added",
24
+ "view_paths" ]
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,41 @@
1
+ require 'haml'
2
+
3
+ module Haml
4
+ class Buffer
5
+ class Jail < Safemode::Jail
6
+ allow :push_script, :push_text, :_hamlout, :open_tag
7
+ end
8
+ end
9
+ end
10
+
11
+ module Haml
12
+ class Engine
13
+ def precompile_for_safemode(filename, ignore_assigns = [], delegate_methods = [])
14
+ @precompiled.gsub!('\\','\\\\\\') # backslashes would disappear in compile_template/modul_eval, so we escape them
15
+
16
+ <<-CODE
17
+ buffer = Haml::Buffer.new(#{options_for_buffer.inspect})
18
+ local_assigns = local_assigns.merge :_hamlout => buffer
19
+
20
+ handler = ActionView::TemplateHandlers::SafeHaml
21
+ assigns = handler.valid_assigns(@template.assigns)
22
+ methods = handler.delegate_methods(self)
23
+ code = %Q(#{code});
24
+
25
+ box = Safemode::Box.new(self, methods, #{filename.inspect}, 0)
26
+ box.eval(code, assigns, local_assigns, &lambda{ yield })
27
+ buffer.buffer
28
+ CODE
29
+
30
+ # preamble = "buffer = Haml::Buffer.new(#{options_for_buffer.inspect})
31
+ # local_assigns = local_assigns.merge :_hamlout => buffer
32
+ # assigns = @template.assigns.reject{|key, value| #{ignore_assigns.inspect}.include?(key) };".gsub("\n", ';')
33
+ #
34
+ # postamble = "box = Safemode::Box.new(self, #{delegate_methods.inspect})
35
+ # box.eval(code, assigns, local_assigns, &lambda{ yield })
36
+ # buffer.buffer".gsub("\n", ';')
37
+ #
38
+ # preamble + "code = %Q(#{@precompiled});" + postamble
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,8 @@
1
+ require 'rubygems'
2
+ require 'ruby2ruby'
3
+
4
+ sexp = ParseTree.translate('a')
5
+ puts Ruby2Ruby.new.process(sexp)
6
+
7
+ # should work, but yields:
8
+ # UnknownNodeError: Bug! Unknown node-type :vcall to Ruby2Ruby
data/lib/safemode.rb ADDED
@@ -0,0 +1,58 @@
1
+ require 'rubygems'
2
+
3
+ require 'ruby2ruby'
4
+ begin
5
+ require 'ruby_parser' # try to load RubyParser and use it if present
6
+ rescue LoadError => e
7
+ end
8
+ # this doesn't work somehow. Maybe something changed inside
9
+ # ParseTree or sexp_processor or so.
10
+ # (the require itself works, but ParseTree doesn't play nice)
11
+ # begin
12
+ # require 'parse_tree'
13
+ # rescue LoadError => e
14
+ # end
15
+
16
+ require 'safemode/core_ext'
17
+ require 'safemode/blankslate'
18
+ require 'safemode/exceptions'
19
+ require 'safemode/jail'
20
+ require 'safemode/core_jails'
21
+ require 'safemode/parser'
22
+ require 'safemode/scope'
23
+
24
+ module Safemode
25
+ class << self
26
+ def jail(obj)
27
+ find_jail_class(obj.class).new obj
28
+ end
29
+
30
+ def find_jail_class(klass)
31
+ while klass != Object
32
+ return klass.const_get('Jail') if klass.const_defined?('Jail')
33
+ klass = klass.superclass
34
+ end
35
+ Jail
36
+ end
37
+ end
38
+
39
+ define_core_jail_classes
40
+
41
+ class Box
42
+ def initialize(delegate = nil, delegate_methods = [], filename = nil, line = nil)
43
+ @scope = Scope.new(delegate, delegate_methods)
44
+ @filename = filename
45
+ @line = line
46
+ end
47
+
48
+ def eval(code, assigns = {}, locals = {}, &block)
49
+ code = Parser.jail(code)
50
+ binding = @scope.bind(assigns, locals, &block)
51
+ result = Kernel.eval(code, binding, @filename || __FILE__, @line || __LINE__)
52
+ end
53
+
54
+ def output
55
+ @scope.output
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,34 @@
1
+ module Safemode
2
+ class Blankslate
3
+ @@allow_instance_methods = ['class', 'inspect', 'methods', 'respond_to?', 'to_s', 'instance_variable_get']
4
+ @@allow_class_methods = ['methods', 'new', 'name', 'inspect', '<', 'ancestors', '=='] # < needed in Rails Object#subclasses_of
5
+
6
+ silently { undef_methods(*instance_methods - @@allow_instance_methods) }
7
+ class << self
8
+ silently { undef_methods(*instance_methods - @@allow_class_methods) }
9
+
10
+ def method_added(name) end # ActiveSupport needs this
11
+
12
+ def inherited(subclass)
13
+ subclass.init_allowed_methods(@allowed_methods)
14
+ end
15
+
16
+ def init_allowed_methods(allowed_methods)
17
+ @allowed_methods = allowed_methods
18
+ end
19
+
20
+ def allowed_methods
21
+ @allowed_methods ||= []
22
+ end
23
+
24
+ def allow(*names)
25
+ @allowed_methods = allowed_methods + names.map{|name| name.to_s}
26
+ @allowed_methods.uniq!
27
+ end
28
+
29
+ def allowed?(name)
30
+ allowed_methods.include? name.to_s
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,39 @@
1
+ module Kernel
2
+ def silently(&blk)
3
+ old_verbose, $VERBOSE = $VERBOSE, nil
4
+ yield
5
+ $VERBOSE = old_verbose
6
+ end
7
+ end
8
+
9
+ class Module
10
+ def undef_methods(*methods)
11
+ methods.each { |name| undef_method(name) }
12
+ end
13
+ end
14
+
15
+ class Object
16
+ def to_jail
17
+ Safemode.jail self
18
+ end
19
+ end
20
+
21
+ # As every call to an object in the eval'ed string will be jailed by the
22
+ # parser we don't need to "proactively" jail arrays and hashes. Likewise we
23
+ # don't need to jail objects returned from a jail. Doing so would provide
24
+ # "double" protection, but it also would break using a return value in an if
25
+ # statement, passing them to a Rails helper etc.
26
+
27
+ # class Array
28
+ # def to_jail
29
+ # Safemode.jail collect{ |obj| obj.to_jail }
30
+ # end
31
+ # end
32
+ #
33
+ # class Hash
34
+ # def to_jail
35
+ # hash = {}
36
+ # collect{ |key, obj| hash[key] = obj.to_jail}
37
+ # Safemode.jail hash
38
+ # end
39
+ # end
@@ -0,0 +1,104 @@
1
+ module Safemode
2
+ class << self
3
+ def define_core_jail_classes
4
+ core_classes.each do |klass|
5
+ define_jail_class(klass).allow *core_jail_methods(klass).uniq
6
+ end
7
+ end
8
+
9
+ def define_jail_class(klass)
10
+ unless klass.const_defined?("Jail")
11
+ klass.const_set("Jail", jail = Class.new(Safemode::Jail))
12
+ end
13
+ klass.const_get('Jail')
14
+ end
15
+
16
+ def core_classes
17
+ klasses = [ Array, Bignum, Fixnum, Float, Hash,
18
+ Range, String, Symbol, Time ]
19
+ klasses << Date if defined? Date
20
+ klasses << DateTime if defined? DateTime
21
+ klasses
22
+ end
23
+
24
+ def core_jail_methods(klass)
25
+ @@methods_whitelist[klass.name] + (@@default_methods & klass.instance_methods)
26
+ end
27
+ end
28
+
29
+ # these methods are allowed in all classes if they are present
30
+ @@default_methods = %w( % & * ** + +@ - -@ / < << <= <=> == === > >= >> ^ | ~
31
+ eql? equal? new methods is_a? kind_of? nil?
32
+ [] []= to_a to_jail to_s inspect to_param )
33
+
34
+ # whitelisted methods for core classes ... kind of arbitrary selection
35
+ @@methods_whitelist = {
36
+ 'Array' => %w(assoc at blank? collect collect! compact compact! concat
37
+ delete delete_at delete_if each each_index empty? fetch
38
+ fill first flatten flatten! hash include? index indexes
39
+ indices inject insert join last length map map! nitems pop
40
+ push rassoc reject reject! reverse reverse! reverse_each
41
+ rindex select shift size slice slice! sort sort! transpose
42
+ uniq uniq! unshift values_at zip),
43
+
44
+ 'Bignum' => %w(abs ceil chr coerce div divmod downto floor hash
45
+ integer? modulo next nonzero? quo remainder round
46
+ singleton_method_added size step succ times to_f to_i
47
+ to_int to_s truncate upto zero?),
48
+
49
+ 'Fixnum' => %w(abs ceil chr coerce div divmod downto floor id2name
50
+ integer? modulo modulo next nonzero? quo remainder round
51
+ singleton_method_added size step succ times to_f to_i
52
+ to_int to_s to_sym truncate upto zero?),
53
+
54
+ 'Float' => %w(abs ceil coerce div divmod finite? floor hash
55
+ infinite? integer? modulo nan? nonzero? quo remainder
56
+ round singleton_method_added step to_f to_i to_int to_s
57
+ truncate zero?),
58
+
59
+ 'Hash' => %w(blank? clear delete delete_if each each_key each_pair
60
+ each_value empty? fetch has_key? has_value? include? index
61
+ invert key? keys length member? merge merge! rec_merge! rehash
62
+ reject reject! select shift size sort store
63
+ update value? values values_at),
64
+
65
+ 'Range' => %w(begin each end exclude_end? first hash include?
66
+ include_without_range? last member? step),
67
+
68
+ 'String' => %w(blank? capitalize capitalize! casecmp center chomp chomp!
69
+ chop chop! concat count crypt delete delete! downcase
70
+ downcase! dump each each_byte each_line empty? end_with? gsub
71
+ gsub! hash hex include? index insert intern iseuc issjis
72
+ isutf8 kconv length ljust lstrip lstrip! match next next! oct
73
+ reverse reverse! rindex rjust rstrip rstrip! scan size slice
74
+ slice! split squeeze squeeze! start_with? strip strip! sub
75
+ sub! succ succ! sum swapcase swapcase! to_f to_i to_str to_sym
76
+ to_xs toeuc tojis tosjis toutf16 toutf8 tr tr! tr_s tr_s!
77
+ upcase upcase! upto),
78
+
79
+ 'Symbol' => %w(to_i to_int),
80
+
81
+ 'Time' => %w(_dump asctime ctime day dst? getgm getlocal getutc gmt?
82
+ gmt_offset gmtime gmtoff hash hour httpdate isdst iso8601
83
+ localtime mday min minus_without_duration mon month
84
+ plus_without_duration rfc2822 rfc822 sec strftime succ to_date
85
+ to_datetime to_f to_i tv_sec tv_usec usec utc utc? utc_offset
86
+ wday xmlschema yday year zone to_formatted_s),
87
+
88
+ 'Date' => %w(ajd amjd asctime ctime cwday cweek cwyear day day_fraction
89
+ default_inspect downto england gregorian gregorian? hash italy
90
+ jd julian julian? ld leap? mday minus_without_duration mjd mon
91
+ month new_start newsg next ns? os? plus_without_duration sg
92
+ start step strftime succ upto wday yday year),
93
+
94
+ 'DateTime' => %w(hour, min, new_offset, newof, of, offset, sec,
95
+ sec_fraction, strftime, to_datetime_default_s, to_json, zone),
96
+
97
+ 'NilClass' => %w(blank? duplicable? to_f to_i),
98
+
99
+ 'FalseClass' => %w(blank? duplicable?),
100
+
101
+ 'TrueClass' => %w(blank? duplicable?)
102
+
103
+ }
104
+ end