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 +22 -0
- data/README.markdown +71 -0
- data/Rakefile +15 -0
- data/demo.rb +23 -0
- data/init.rb +1 -0
- data/lib/action_view/template_handlers/safe_erb.rb +43 -0
- data/lib/action_view/template_handlers/safe_haml.rb +27 -0
- data/lib/action_view/template_handlers/safemode_handler.rb +28 -0
- data/lib/haml/safemode.rb +41 -0
- data/lib/rubyparser_bug.rb +8 -0
- data/lib/safemode.rb +58 -0
- data/lib/safemode/blankslate.rb +34 -0
- data/lib/safemode/core_ext.rb +39 -0
- data/lib/safemode/core_jails.rb +104 -0
- data/lib/safemode/exceptions.rb +22 -0
- data/lib/safemode/jail.rb +28 -0
- data/lib/safemode/parser.rb +196 -0
- data/lib/safemode/scope.rb +58 -0
- data/safemode.gemspec +14 -0
- data/safemode.rb +1 -0
- data/test/test_all.rb +14 -0
- data/test/test_erb_eval.rb +76 -0
- data/test/test_helper.rb +130 -0
- data/test/test_jail.rb +53 -0
- data/test/test_safemode_eval.rb +68 -0
- data/test/test_safemode_parser.rb +45 -0
- metadata +98 -0
@@ -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
|
data/test/test_helper.rb
ADDED
@@ -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
|