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.

@@ -0,0 +1,22 @@
1
+ module Safemode
2
+ class Error < RuntimeError; end
3
+
4
+ class SecurityError < Error
5
+ @@types = { :const => 'constant',
6
+ :xstr => 'shell command',
7
+ :fcall => 'method',
8
+ :vcall => 'method',
9
+ :gvar => 'global variable' }
10
+
11
+ def initialize(type, value = nil)
12
+ type = @@types[type] if @@types.include?(type)
13
+ super "Safemode doesn't allow to access '#{type}'" + (value ? " on #{value}" : '')
14
+ end
15
+ end
16
+
17
+ class NoMethodError < Error
18
+ def initialize(method, jail, source = nil)
19
+ super "undefined method '#{method}' for #{jail}" + (source ? " (#{source})" : '')
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ module Safemode
2
+ class Jail < Blankslate
3
+ def initialize(source = nil)
4
+ @source = source
5
+ end
6
+
7
+ def to_jail
8
+ self
9
+ end
10
+
11
+ def to_s
12
+ @source.to_s
13
+ end
14
+
15
+ def method_missing(method, *args, &block)
16
+ unless self.class.allowed?(method)
17
+ raise Safemode::NoMethodError.new(method, self.class.name, @source.class.name)
18
+ end
19
+
20
+ # As every call to an object in the eval'ed string will be jailed by the
21
+ # parser we don't need to "proactively" jail arrays and hashes. Likewise we
22
+ # don't need to jail objects returned from a jail. Doing so would provide
23
+ # "double" protection, but it also would break using a return value in an if
24
+ # statement, passing them to a Rails helper etc.
25
+ @source.send(method, *args, &block)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,196 @@
1
+ module Safemode
2
+ class Parser < Ruby2Ruby
3
+ # @@parser = defined?(RubyParser) ? 'RubyParser' : 'ParseTree'
4
+ @@parser = 'RubyParser'
5
+
6
+ class << self
7
+ def jail(code, allowed_fcalls = [])
8
+ @@allowed_fcalls = allowed_fcalls
9
+ tree = parse code
10
+ self.new.process(tree)
11
+ end
12
+
13
+ def parse(code)
14
+ case @@parser
15
+ # when 'ParseTree'
16
+ # ParseTree.translate(code)
17
+ when 'RubyParser'
18
+ RubyParser.new.parse(code)
19
+ else
20
+ raise "unknown parser #{@@parser}"
21
+ end
22
+ end
23
+
24
+ def parser=(parser)
25
+ @@parser = parser
26
+ end
27
+ end
28
+
29
+ def jail(str, parentheses = false)
30
+ str = parentheses ? "(#{str})." : "#{str}." if str
31
+ "#{str}to_jail"
32
+ end
33
+
34
+ # split up #process_call. see below ...
35
+ def process_call(exp)
36
+ receiver = jail process_call_receiver(exp)
37
+ name = exp.shift
38
+ args = process_call_args(exp)
39
+ process_call_code(receiver, name, args)
40
+ end
41
+
42
+ def process_fcall(exp)
43
+ # using haml we probably never arrive here because :lasgn'ed :fcalls
44
+ # somehow seem to change to :calls somewhere during processing
45
+ # unless @@allowed_fcalls.include?(exp.first)
46
+ # code = Ruby2Ruby.new.process([:fcall, exp[1], exp[2]]) # wtf ...
47
+ # raise_security_error(exp.first, code)
48
+ # end
49
+ "to_jail.#{super}"
50
+ end
51
+
52
+ def process_vcall(exp)
53
+ # unless @@allowed_fcalls.include?(exp.first)
54
+ # code = Ruby2Ruby.new.process([:fcall, exp[1], exp[2]]) # wtf ...
55
+ # raise_security_error(exp.first, code)
56
+ # end
57
+ name = exp[1]
58
+ exp.clear
59
+ "to_jail.#{name}"
60
+ end
61
+
62
+ def process_iasgn(exp)
63
+ code = super
64
+ if code != '@output_buffer = ""'
65
+ raise_security_error(:iasgn, code)
66
+ else
67
+ code
68
+ end
69
+ end
70
+
71
+ # see http://www.namikilab.tuat.ac.jp/~sasada/prog/rubynodes/nodes.html
72
+
73
+ allowed = [ :call, :vcall, :evstr,
74
+ :lvar, :dvar, :ivar, :lasgn, :masgn, :dasgn, :dasgn_curr,
75
+ :lit, :str, :dstr, :dsym, :nil, :true, :false,
76
+ :array, :zarray, :hash, :dot2, :dot3, :flip2, :flip3,
77
+ :if, :case, :when, :while, :until, :iter, :for, :break, :next, :yield,
78
+ :and, :or, :not,
79
+ :iasgn, # iasgn is sometimes allowed
80
+ # not sure about self ...
81
+ :self,
82
+ # unnecessarily advanced?
83
+ :argscat, :argspush, :splat, :block_pass,
84
+ :op_asgn1, :op_asgn2, :op_asgn_and, :op_asgn_or,
85
+ # needed for haml
86
+ :block ]
87
+
88
+ disallowed = [ # :self, # self doesn't seem to be needed for vcalls?
89
+ :const, :defn, :defs, :alias, :valias, :undef, :class, :attrset,
90
+ :module, :sclass, :colon2, :colon3,
91
+ :fbody, :scope, :args, :block_arg, :postexe,
92
+ :redo, :retry, :begin, :rescue, :resbody, :ensure,
93
+ :defined, :super, :zsuper, :return,
94
+ :dmethod, :bmethod, :to_ary, :svalue, :match,
95
+ :attrasgn, :cdecl, :cvasgn, :cvdecl, :cvar, :gvar, :gasgn,
96
+ :xstr, :dxstr,
97
+ # not sure how secure ruby regexp is, so leave it out for now
98
+ :dregx, :dregx_once, :match2, :match3, :nth_ref, :back_ref ]
99
+
100
+ # SexpProcessor bails when we overwrite these ... but they are listed as
101
+ # "internal nodes that you can't get to" in sexp_processor.rb
102
+ # :ifunc, :method, :last, :opt_n, :cfunc, :newline, :alloca, :memo, :cref
103
+
104
+ disallowed.each do |name|
105
+ define_method "process_#{name}" do
106
+ code = super
107
+ raise_security_error(name, code)
108
+ end
109
+ end
110
+
111
+ def raise_security_error(type, info)
112
+ raise Safemode::SecurityError.new(type, info)
113
+ end
114
+
115
+ # split up Ruby2Ruby#process_call monster method so we can hook into it
116
+ # in a more readable manner
117
+
118
+ def process_call_receiver(exp)
119
+ receiver_node_type = exp.first.nil? ? nil : exp.first.first
120
+ receiver = process exp.shift
121
+ receiver = "(#{receiver})" if
122
+ Ruby2Ruby::ASSIGN_NODES.include? receiver_node_type
123
+ receiver
124
+ end
125
+
126
+ def process_call_args(exp)
127
+ args_exp = exp.shift rescue nil
128
+ if args_exp && args_exp.first == :array # FIX
129
+ args = "#{process(args_exp)[1..-2]}"
130
+ else
131
+ args = process args_exp
132
+ args = nil if args.empty?
133
+ end
134
+ args
135
+ end
136
+
137
+ def process_call_code(receiver, name, args)
138
+ case name
139
+ when :<=>, :==, :<, :>, :<=, :>=, :-, :+, :*, :/, :%, :<<, :>>, :** then
140
+ "(#{receiver} #{name} #{args})"
141
+ when :[] then
142
+ "#{receiver}[#{args}]"
143
+ when :"-@" then
144
+ "-#{receiver}"
145
+ when :"+@" then
146
+ "+#{receiver}"
147
+ else
148
+ unless receiver.nil? then
149
+ "#{receiver}.#{name}#{args ? "(#{args})" : args}"
150
+ else
151
+ "#{name}#{args ? "(#{args})" : args}"
152
+ end
153
+ end
154
+ end
155
+
156
+ # Ruby2Ruby process_if rewrites if and unless statements in a way that
157
+ # makes the result unusable for evaluation in, e.g. ERB which appends a
158
+ # call to to_s when using <%= %> tags. We'd need to either enclose the
159
+ # result from process_if into parentheses like (1 if true) and
160
+ # (true ? (1) : (2)) or just use the plain if-then-else-end syntax (so
161
+ # that ERB can safely append to_s to the resulting block).
162
+
163
+ def process_if(exp)
164
+ expand = Ruby2Ruby::ASSIGN_NODES.include? exp.first.first
165
+ c = process exp.shift
166
+ t = process exp.shift
167
+ f = process exp.shift
168
+
169
+ c = "(#{c.chomp})" if c =~ /\n/
170
+
171
+ if t then
172
+ # unless expand then
173
+ # if f then
174
+ # r = "#{c} ? (#{t}) : (#{f})"
175
+ # r = nil if r =~ /return/ # HACK - need contextual awareness or something
176
+ # else
177
+ # r = "#{t} if #{c}"
178
+ # end
179
+ # return r if r and (@indent+r).size < LINE_LENGTH and r !~ /\n/
180
+ # end
181
+
182
+ r = "if #{c} then\n#{indent(t)}\n"
183
+ r << "else\n#{indent(f)}\n" if f
184
+ r << "end"
185
+
186
+ r
187
+ else
188
+ # unless expand then
189
+ # r = "#{f} unless #{c}"
190
+ # return r if (@indent+r).size < LINE_LENGTH and r !~ /\n/
191
+ # end
192
+ "unless #{c} then\n#{indent(f)}\nend"
193
+ end
194
+ end
195
+ end
196
+ end
@@ -0,0 +1,58 @@
1
+ module Safemode
2
+ class Scope < Blankslate
3
+ def initialize(delegate = nil, delegate_methods = [])
4
+ @delegate = delegate
5
+ @delegate_methods = delegate_methods
6
+ @locals = {}
7
+ end
8
+
9
+ def bind(instance_vars = {}, locals = {}, &block)
10
+ @locals = symbolize_keys(locals) # why can't I just pull them to local scope in the same way like instance_vars?
11
+ instance_vars = symbolize_keys(instance_vars)
12
+ instance_vars.each {|key, obj| eval "@#{key} = instance_vars[:#{key}]" }
13
+ @_safemode_output = ''
14
+ binding
15
+ end
16
+
17
+ def to_jail
18
+ self
19
+ end
20
+
21
+ def puts(*args)
22
+ print args.to_s + "\n"
23
+ end
24
+
25
+ def print(*args)
26
+ @_safemode_output += args.to_s
27
+ end
28
+
29
+ def output
30
+ @_safemode_output
31
+ end
32
+
33
+ def method_missing(method, *args, &block)
34
+ if @locals.has_key?(method)
35
+ @locals[method]
36
+ elsif @delegate_methods.include?(method)
37
+ @delegate.send method, *unjail_args(args), &block
38
+ else
39
+ raise Safemode::SecurityError.new(method, "#<Safemode::ScopeObject>")
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def symbolize_keys(hash)
46
+ hash.inject({}) do |hash, (key, value)|
47
+ hash[key.to_s.intern] = value
48
+ hash
49
+ end
50
+ end
51
+
52
+ def unjail_args(args)
53
+ args.collect do |arg|
54
+ arg.class.name =~ /::Jail$/ ? arg.instance_variable_get(:@source) : arg
55
+ end
56
+ end
57
+ end
58
+ end
data/safemode.gemspec ADDED
@@ -0,0 +1,14 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = %q{safemode}
3
+ s.version = "0.0.2"
4
+ s.date = %q{2011-12-17}
5
+ s.authors = ["sven fuchs, peter cooper, kingsley hendrickse"]
6
+ s.email = %q{kingsley@mindflowsolutions.com}
7
+ s.summary = %q{Safemode provides a simple sandbox for executing eval ruby code, as well as erb and haml}
8
+ s.homepage = %q{https://github.com/svenfuchs/safemode}
9
+ s.description = %q{Safemode provides a simple sandbox for executing eval ruby code, as well as erb and haml. Written by Sven Fuchs and Peter Cooper and packaged into a gem by Kingsley Hendrickse}
10
+ s.add_dependency('ruby2ruby')
11
+ s.files = Dir['lib/**/*.rb'] + Dir['*']
12
+ s.files += Dir['test/**/*.rb']
13
+ s.rubyforge_project = 'safemode'
14
+ end
data/safemode.rb ADDED
@@ -0,0 +1 @@
1
+ require File.dirname(__FILE__) + '/lib/safemode.rb'
data/test/test_all.rb ADDED
@@ -0,0 +1,14 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+ Test::Unit.run = false
3
+
4
+ require File.join(File.dirname(__FILE__), 'test_jail')
5
+ require File.join(File.dirname(__FILE__), 'test_safemode_parser')
6
+ require File.join(File.dirname(__FILE__), 'test_safemode_eval')
7
+ require File.join(File.dirname(__FILE__), 'test_erb_eval')
8
+
9
+ # ['ParseTree', 'RubyParser'].each do |parser|
10
+ ['RubyParser'].each do |parser|
11
+ Safemode::Parser.parser = parser
12
+ puts "Running suite with Safemode::Parser using #{parser}"
13
+ Test::Unit::AutoRunner.run
14
+ end
@@ -0,0 +1,76 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ class TestSafemodeEval < Test::Unit::TestCase
4
+ include TestHelper
5
+
6
+ def setup
7
+ @box = Safemode::Box.new
8
+ @locals = { :article => Article.new }
9
+ @assigns = { :article => Article.new }
10
+ @erb_parse = lambda {|code| ERB.new("<%= #{code} %>").src }
11
+ end
12
+
13
+ def test_some_stuff_that_should_work
14
+ ['"test".upcase', '10.succ', '10.times{}', '[1,2,3].each{|a| a + 1}', 'true ? 1 : 0', 'a = 1'].each do |code|
15
+ code = ERB.new("<%= #{code} %>").src
16
+ assert_nothing_raised{ @box.eval code }
17
+ end
18
+ end
19
+
20
+ def test_should_turn_assigns_to_jails
21
+ assert_raise_no_method "@article.system", @assigns, &@erb_parse
22
+ end
23
+
24
+ def test_should_turn_locals_to_jails
25
+ code = @erb_parse.call "article.system"
26
+ assert_raise(Safemode::NoMethodError){ @box.eval code, {}, @locals }
27
+ end
28
+
29
+ def test_should_allow_method_access_on_assigns
30
+ code = @erb_parse.call "@article.title"
31
+ assert_nothing_raised{ @box.eval code, @assigns }
32
+ end
33
+
34
+ def test_should_allow_method_access_on_locals
35
+ code = @erb_parse.call "article.title"
36
+ assert_nothing_raised{ @box.eval code, {}, @locals }
37
+ end
38
+
39
+ def test_should_not_raise_on_if_using_return_values
40
+ code = @erb_parse.call "if @article.is_article?\n 1\n end"
41
+ assert_nothing_raised{ @box.eval code, @assigns }
42
+ end
43
+
44
+ def test_should_work_with_if_using_return_values
45
+ code = @erb_parse.call "if @article.is_article? then 1 end"
46
+ assert_equal @box.eval(code, @assigns), "1" # ERB calls to_s on the result of the if block
47
+ end
48
+
49
+ def test__FILE__should_not_render_filename
50
+ code = @erb_parse.call "__FILE__"
51
+ assert_equal '(string)', @box.eval(code)
52
+ end
53
+
54
+ def test_interpolated_xstr_should_raise_security
55
+ assert_raise_security '"#{`ls -a`}"'
56
+ end
57
+
58
+ TestHelper.no_method_error_raising_calls.each do |call|
59
+ call.gsub!('"', '\\\\"')
60
+ class_eval %Q(
61
+ def test_calling_#{call.gsub(/[\W]/, '_')}_should_raise_no_method
62
+ assert_raise_no_method "#{call}"
63
+ end
64
+ )
65
+ end
66
+
67
+ TestHelper.security_error_raising_calls.each do |call|
68
+ call.gsub!('"', '\\\\"')
69
+ class_eval %Q(
70
+ def test_calling_#{call.gsub(/[\W]/, '_')}_should_raise_security
71
+ assert_raise_security "#{call}"
72
+ end
73
+ )
74
+ end
75
+
76
+ end
@@ -0,0 +1,130 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
2
+
3
+ require 'rubygems'
4
+ require 'test/unit'
5
+ require 'safemode'
6
+ require 'erb'
7
+
8
+ module TestHelper
9
+ class << self
10
+ def no_method_error_raising_calls
11
+ [ 'nil.eval("a = 1")',
12
+ 'true.eval("a = 1")',
13
+ 'false.eval("a = 1")',
14
+ '@article.is_article?.eval("a = 1")',
15
+ '@article.comments.map{|c| c.eval("a = 1")}' ]
16
+ end
17
+
18
+ def security_error_raising_calls
19
+ [ "class A\n end",
20
+ 'File.open("/etc/passwd")',
21
+ '::File.open("/etc/passwd")',
22
+ 'defined? a',
23
+ # '"#{`ls -a`}"', # hu? testing this separately, see testcase
24
+ 'alias b instance_eval',
25
+ '@@a',
26
+ '@@a = 1',
27
+ '$LOAD_PATH',
28
+ '$LOAD_PATH = 1',
29
+ '@a = 1',
30
+ '$1',
31
+ 'public to_s',
32
+ 'protected to_s',
33
+ 'private to_s',
34
+ "attr_reader :a",
35
+ 'URI("http://google.com")',
36
+ "`ls -a`", "exec('echo *')", "syscall 4, 1, 'hello', 5", "system('touch /tmp/helloworld')",
37
+ "abort",
38
+ "exit(0)", "exit!(0)", "at_exit{'goodbye'}",
39
+ "autoload(::MyModule, 'my_module.rb')",
40
+ "binding",
41
+ "callcc{|cont| cont.call}",
42
+ 'eval %Q(send(:system, "ls -a"))',
43
+ "fork",
44
+ "gets", "readline", "readlines",
45
+ "global_variables", "local_variables",
46
+ "proc{}",
47
+ "lambda{}",
48
+ "load('/path/to/file')", "require 'something'",
49
+ "loop{}",
50
+ "open('/etc/passwd'){|f| f.read}",
51
+ "p 'text'", "pretty_inspect",
52
+ # "print 'text'", "puts 'text'", allowed and buffered these (see ScopeObject)
53
+ "printf 'text'", "putc 'a'",
54
+ "raise RuntimeError, 'should not happen'",
55
+ "rand(0)", "srand(0)",
56
+ "set_trace_func proc{|event| puts event}", "trace_var :$_, proc {|v| puts v }", "untrace_var :$_",
57
+ "sleep", "sleep(0)",
58
+ "test(1, a, b)",
59
+ "Signal.trap(0, proc { puts 'Terminating: #{$$}' })",
60
+ "warn 'warning'" ]
61
+ end
62
+ end
63
+
64
+ def assert_raise_no_method(code = nil, assigns = {}, locals = {}, &block)
65
+ assert_raise_safemode_error(Safemode::NoMethodError, code, assigns, locals, &block)
66
+ end
67
+
68
+ def assert_raise_security(code = nil, assigns = {}, locals = {}, &block)
69
+ assert_raise_safemode_error(Safemode::SecurityError, code, assigns, locals, &block)
70
+ end
71
+
72
+ def assert_raise_safemode_error(error, code, assigns = {}, locals = {})
73
+ code = yield(code) if block_given?
74
+ assert_raise(error, code) { safebox_eval(code, assigns, locals) }
75
+ end
76
+
77
+ def safebox_eval(code, assigns = {}, locals = {})
78
+ # puts Safemode::Parser.jail(code)
79
+ Safemode::Box.new.eval code, assigns, locals
80
+ end
81
+ end
82
+
83
+ class Article
84
+ def is_article?
85
+ true
86
+ end
87
+
88
+ def title
89
+ 'an article title'
90
+ end
91
+
92
+ def to_jail
93
+ Article::Jail.new self
94
+ end
95
+
96
+ def comments
97
+ [Comment.new(self), Comment.new(self)]
98
+ end
99
+ end
100
+
101
+ class Comment
102
+ attr_reader :article
103
+
104
+ def initialize(article)
105
+ @article = article
106
+ end
107
+
108
+ def text
109
+ "comment #{object_id}"
110
+ end
111
+
112
+ def to_jail
113
+ Comment::Jail.new self
114
+ end
115
+ end
116
+
117
+ class Article::Jail < Safemode::Jail
118
+ allow :title, :comments, :is_article?
119
+
120
+ def author_name
121
+ "this article's author name"
122
+ end
123
+ end
124
+
125
+ class Article::ExtendedJail < Article::Jail
126
+ end
127
+
128
+ class Comment::Jail < Safemode::Jail
129
+ allow :article, :text
130
+ end