sandboxed_erb 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,45 @@
1
+ =begin
2
+
3
+ This file is part of the sandboxed_erb project, https://github.com/markpent/SandboxedERB
4
+
5
+ Copyright (c) 2011 Mark Pentland <mark.pent@gmail.com>
6
+
7
+ sandboxed_erb is free software: you can redistribute it and/or modify
8
+ it under the terms of the gnu general public license as published by
9
+ the free software foundation, either version 3 of the license, or
10
+ (at your option) any later version.
11
+
12
+ sandboxed_erb is distributed in the hope that it will be useful,
13
+ but without any warranty; without even the implied warranty of
14
+ merchantability or fitness for a particular purpose. see the
15
+ gnu general public license for more details.
16
+
17
+ you should have received a copy of the gnu general public license
18
+ along with shikashi. if not, see <http://www.gnu.org/licenses/>.
19
+
20
+ =end
21
+
22
+
23
+ require "erb"
24
+ require "partialruby"
25
+ require "sandboxed_erb/template"
26
+ require "sandboxed_erb/tree_processor"
27
+ require "sandboxed_erb/sandbox_methods"
28
+ require "sandboxed_erb/system_mixins"
29
+
30
+
31
+ module SandboxedErb
32
+ class Error < ::StandardError #:nodoc: all
33
+ end
34
+
35
+ class CompileError < Error #:nodoc: all
36
+ end
37
+ class CompileSecurityError < Error #:nodoc: all
38
+ end
39
+ class RuntimeError < Error #:nodoc: all
40
+ end
41
+ class RuntimeSecurityError < Error #:nodoc: all
42
+ end
43
+ class MissingMethodError < Error #:nodoc: all
44
+ end
45
+ end
@@ -0,0 +1,93 @@
1
+ =begin
2
+
3
+ This file is part of the sandboxed_erb project, https://github.com/markpent/SandboxedERB
4
+
5
+ Copyright (c) 2011 Mark Pentland <mark.pent@gmail.com>
6
+
7
+ sandboxed_erb is free software: you can redistribute it and/or modify
8
+ it under the terms of the gnu general public license as published by
9
+ the free software foundation, either version 3 of the license, or
10
+ (at your option) any later version.
11
+
12
+ sandboxed_erb is distributed in the hope that it will be useful,
13
+ but without any warranty; without even the implied warranty of
14
+ merchantability or fitness for a particular purpose. see the
15
+ gnu general public license for more details.
16
+
17
+ you should have received a copy of the gnu general public license
18
+ along with shikashi. if not, see <http://www.gnu.org/licenses/>.
19
+
20
+ =end
21
+
22
+ #Adds the sandboxed_methods and not_sandboxed_methods to Module class so it is avialble to all classes.
23
+ class Module
24
+
25
+ #Specify what methods you want to be accessable from the sandbox.
26
+ #
27
+ #Example
28
+ # class SomeClass
29
+ # sandboxed_methods :some_method
30
+ # def some_method
31
+ # "this is ok to call"
32
+ # end
33
+ # end
34
+ #
35
+ #If the object has a method called set_sandbox_context, it will be passed the sandbox_context, which is a map containing the context passed to SandboxedErb::Template.run, as well as another entry keys to :locals which contains the locals map passed to run.
36
+ def sandboxed_methods(*allowed_methods)
37
+ _sb_allowed_methods_map = {}
38
+ allowed_methods.each { |meth|
39
+ _sb_allowed_methods_map[meth.to_s.intern] = true
40
+ }
41
+
42
+ define_method :_sbm do |meth, sandbox_context, *args|
43
+ if _sb_allowed_methods_map[meth]
44
+ self.set_sandbox_context(sandbox_context) if self.respond_to?(:set_sandbox_context)
45
+ begin
46
+ self.__send__(meth, *args)
47
+ rescue Exception=>e
48
+ raise "Error calling #{meth}: #{e.message}"
49
+ end
50
+ else
51
+ puts _sb_allowed_methods_map.inspect if $DEBUG
52
+ raise SandboxedErb::MissingMethodError, "Unknown method '#{meth}' on object '#{self.class.name}'"
53
+ end
54
+ end
55
+ end
56
+
57
+
58
+ #Shortcut to allow everything except a few methods
59
+ #
60
+ #This will not include any superclass methods unless include_superclasses = true
61
+ #
62
+ #Example
63
+ # class SomeClass
64
+ # not_sandboxed_methods :unsafe_method
65
+ # def some_method
66
+ # "this is ok to call"
67
+ # end
68
+ # def unsafe_method
69
+ # "this is NOT ok to call"
70
+ # end
71
+ # end
72
+ def not_sandboxed_methods(include_superclasses = false, *disallowed_methods)
73
+
74
+ __the_methods_to_check = public_instance_methods(false)
75
+ if include_superclasses
76
+ clz = self.superclass
77
+ while !clz.nil?
78
+ unless clz == Object
79
+ __the_methods_to_check += clz.public_instance_methods(false)
80
+ end
81
+ clz = clz.superclass
82
+ end
83
+ end
84
+
85
+
86
+ __the_methods_to_check.uniq!
87
+
88
+ sandboxed_methods(*__the_methods_to_check)
89
+
90
+ end
91
+
92
+ end
93
+
@@ -0,0 +1,37 @@
1
+ =begin
2
+
3
+ This file is part of the sandboxed_erb project, https://github.com/markpent/SandboxedERB
4
+
5
+ Copyright (c) 2011 Mark Pentland <mark.pent@gmail.com>
6
+
7
+ sandboxed_erb is free software: you can redistribute it and/or modify
8
+ it under the terms of the gnu general public license as published by
9
+ the free software foundation, either version 3 of the license, or
10
+ (at your option) any later version.
11
+
12
+ sandboxed_erb is distributed in the hope that it will be useful,
13
+ but without any warranty; without even the implied warranty of
14
+ merchantability or fitness for a particular purpose. see the
15
+ gnu general public license for more details.
16
+
17
+ you should have received a copy of the gnu general public license
18
+ along with shikashi. if not, see <http://www.gnu.org/licenses/>.
19
+
20
+ =end
21
+
22
+
23
+ #add sandboxed method to basic inbuilt objects
24
+
25
+ String.not_sandboxed_methods true
26
+ Fixnum.not_sandboxed_methods true
27
+ Float.not_sandboxed_methods true
28
+ Range.not_sandboxed_methods true
29
+ Symbol.not_sandboxed_methods true
30
+ Time.not_sandboxed_methods true
31
+ Date.not_sandboxed_methods true
32
+ DateTime.not_sandboxed_methods true
33
+ NilClass.not_sandboxed_methods true
34
+ Array.not_sandboxed_methods true
35
+ Hash.not_sandboxed_methods true
36
+ FalseClass.not_sandboxed_methods true
37
+ TrueClass.not_sandboxed_methods true
@@ -0,0 +1,224 @@
1
+ =begin
2
+
3
+ This file is part of the sandboxed_erb project, https://github.com/markpent/SandboxedERB
4
+
5
+ Copyright (c) 2011 Mark Pentland <mark.pent@gmail.com>
6
+
7
+ sandboxed_erb is free software: you can redistribute it and/or modify
8
+ it under the terms of the gnu general public license as published by
9
+ the free software foundation, either version 3 of the license, or
10
+ (at your option) any later version.
11
+
12
+ sandboxed_erb is distributed in the hope that it will be useful,
13
+ but without any warranty; without even the implied warranty of
14
+ merchantability or fitness for a particular purpose. see the
15
+ gnu general public license for more details.
16
+
17
+ you should have received a copy of the gnu general public license
18
+ along with shikashi. if not, see <http://www.gnu.org/licenses/>.
19
+
20
+ =end
21
+
22
+ require "erb"
23
+ require "partialruby"
24
+
25
+ module SandboxedErb
26
+
27
+ #This class represents a template which can be compiled then run multiple times.
28
+ #
29
+ #When declaring a template, pass an array of Mixin classes to the contructor to allow the template access to the Mixin methods.
30
+ #
31
+ #Example
32
+ # module ExampleHelper
33
+ # def format_date(date, format)
34
+ # if format == :short_date
35
+ # date.strftime("%d %b %Y %H:%M")
36
+ # else
37
+ # "unknown format: #{format}"
38
+ # end
39
+ # end
40
+ #
41
+ # def current_time
42
+ # DateTime.now
43
+ # end
44
+ # end
45
+ #
46
+ # template = SandboxedErb::Template.new([ExampleHelper])
47
+ # #the template will now have access to the format_date() and current_time() helper function
48
+ # template.compile('the date = <%=format_date(current_time, :short_date)%>')
49
+
50
+ class Template
51
+
52
+ #minins is an array of helper classes which expose methods to the template
53
+ def initialize(mixins = [])
54
+ @mixins = mixins.collect { |clz| "include #{clz.name}"}.join("\n")
55
+ end
56
+
57
+ #compile the template
58
+ #
59
+ #if the template does not compile, false is returned and get_error should be called to get the compile error.
60
+ def compile(str_template)
61
+
62
+ erb_template = compile_erb_template(str_template)
63
+ return false if erb_template.nil?
64
+ #we now have a normal compile erb template (which is just ruby code)
65
+
66
+ sandboxed_compiled_template = sandbox_code(erb_template)
67
+ puts sandboxed_compiled_template if $DEBUG
68
+ return false if sandboxed_compiled_template.nil?
69
+
70
+ @clazz_name = "SandboxedErb::TClass#{self.object_id}"
71
+ @file_name = "tclass_#{self.object_id}"
72
+
73
+ clazz_str = <<-EOF
74
+ class #{@clazz_name} < SandboxedErb::TemplateBase
75
+ #{@mixins}
76
+ def run_internal()
77
+ #{sandboxed_compiled_template}
78
+ end
79
+ end
80
+ #{@clazz_name}.new
81
+ EOF
82
+
83
+ begin
84
+ @template_runner = eval(clazz_str, nil, @file_name)
85
+ rescue Exception=>e
86
+ @error = "Invalid code generated: #{e.message}"
87
+ return false
88
+ end
89
+
90
+ true
91
+
92
+ end
93
+
94
+ #run a compiled template
95
+ #* context: A map of context objects that will be available to helper functions and instance variables, and available to sandboxed objects through the set_sandbox_context callback.
96
+ #* locals: A map of local objects that will be available to the template, and available to sandboxed objects through the set_sandbox_context callback as the :locals entry.
97
+ #If the template runs successfully, the geneated content is returned. If an error occures, nil is returned and get_error should be called to get the error information.
98
+ def run(context, locals)
99
+ begin
100
+ @template_runner.run(context, locals)
101
+ rescue Exception=>e
102
+ @error = e.message
103
+ nil
104
+ end
105
+ end
106
+
107
+ def compile_erb_template(str_template) #:nodoc:
108
+ ecompiler = ERB::Compiler.new(nil)
109
+
110
+ ecompiler.put_cmd = "_erbout.concat"
111
+ ecompiler.insert_cmd = "_erbout.concat"
112
+
113
+ cmd = []
114
+ cmd.push "_erbout = ''"
115
+
116
+ ecompiler.pre_cmd = cmd
117
+
118
+ cmd = []
119
+ cmd.push('_erbout')
120
+
121
+ ecompiler.post_cmd = cmd
122
+ ecompiler.compile(str_template)
123
+ end
124
+
125
+ def sandbox_code(erb_template) #:nodoc:
126
+ @error = nil
127
+ tree = nil
128
+ begin
129
+ tree = RubyParser.new.parse erb_template
130
+ rescue Exception=>e
131
+ #the message is pretty useless.. lets eval the code (it wont get executed because of the compile error)
132
+ begin
133
+ #extra bit of caution.. run in $SAFE=4
134
+ t = Thread.new {
135
+ $SAFE = 4
136
+ eval(erb_template, nil, "line")
137
+ }
138
+ t.join
139
+ rescue Exception=>e2
140
+ @error = e2.message
141
+ return nil
142
+ end
143
+ #if we got here then somehow code that would not compile using RubyParser eval'ed ok...
144
+ throw "SYSTEM ERROR: you may be owned! Code that should not be able to compile has run!"
145
+ end
146
+ begin
147
+ context = PartialRuby::PureRubyContext.new
148
+ tree_processor = SandboxedErb::TreeProcessor.new()
149
+
150
+ tree = tree_processor.process(tree)
151
+ emulationcode = context.emul tree
152
+ rescue Exception=>e
153
+ @error = e.message
154
+ return nil
155
+ end
156
+ emulationcode
157
+ end
158
+
159
+ def get_error
160
+ @error
161
+ end
162
+ end
163
+
164
+ class TemplateBase #:nodoc: all
165
+ def initialize
166
+ @_allowed_methods = {}
167
+ self.class.included_modules.each { |mod|
168
+ unless mod == Kernel
169
+ mod.public_instance_methods.each { |m|
170
+ @_allowed_methods[m.intern] = true
171
+ }
172
+ end
173
+ }
174
+ end
175
+
176
+ def run(context, locals)
177
+ context = {} if context.nil?
178
+ context[:locals] = locals
179
+ unless context.nil?
180
+ for k in context.keys
181
+ eval("@#{k} = context[k]")
182
+ end
183
+ end
184
+ @_sb_context = context
185
+ @_locals = locals
186
+ @_line = 1
187
+ begin
188
+ run_internal
189
+ rescue Exception=>e
190
+ raise "Error on line #{@_line}: #{e.message}"
191
+ ensure
192
+ #cleanup the context
193
+ unless context.nil?
194
+ for k in context.keys
195
+ eval("@#{k} = nil")
196
+ end
197
+ end
198
+ @_sb_context = nil
199
+ @_locals = nil
200
+ end
201
+ end
202
+
203
+ def _get_local(*args)
204
+ target = args.shift
205
+ #check if the target is in the context
206
+ if @_locals[target]
207
+ @_locals[target]
208
+ elsif @_allowed_methods[target] #check if the target is defined in one of the mixin helper functions
209
+ begin
210
+ self.send(target, *args)
211
+ rescue Exception=>e
212
+ raise "Error calling #{target}: #{e.message}"
213
+ end
214
+ else
215
+ raise "Unknown method: #{target}"
216
+ end
217
+ end
218
+
219
+ def _sln(line_no)
220
+ @_line = line_no
221
+ end
222
+ end
223
+
224
+ end
@@ -0,0 +1,215 @@
1
+ =begin
2
+
3
+ This file is part of the sandboxed_erb project, https://github.com/markpent/SandboxedERB
4
+
5
+ Copyright (c) 2011 Mark Pentland <mark.pent@gmail.com>
6
+
7
+ sandboxed_erb is free software: you can redistribute it and/or modify
8
+ it under the terms of the gnu general public license as published by
9
+ the free software foundation, either version 3 of the license, or
10
+ (at your option) any later version.
11
+
12
+ sandboxed_erb is distributed in the hope that it will be useful,
13
+ but without any warranty; without even the implied warranty of
14
+ merchantability or fitness for a particular purpose. see the
15
+ gnu general public license for more details.
16
+
17
+ you should have received a copy of the gnu general public license
18
+ along with shikashi. if not, see <http://www.gnu.org/licenses/>.
19
+
20
+ =end
21
+
22
+ require "ruby_parser"
23
+ require "partialruby"
24
+ require "sexp_processor"
25
+
26
+ module SandboxedErb
27
+ class TreeProcessor < SexpProcessor #:nodoc: all
28
+
29
+ def initialize
30
+ super()
31
+ self.default_method = :fallback_process
32
+ self.require_empty = false
33
+ self.warn_on_default = false
34
+
35
+ @hook_handler_name = "@_hook_handler".intern
36
+ @last_line_number = 0
37
+ end
38
+
39
+ #we treat this same as a call
40
+ def process_attrasgn(tree)
41
+ process_call(tree)
42
+ end
43
+
44
+ def process_call(tree)
45
+ puts tree.inspect if $DEBUG
46
+ if [:_sbm].include?(tree[2])
47
+ raise SandboxedErb::CompileSecurityError, "Line #{tree.line}: #{tree[2].to_s} is a reserved method"
48
+ elsif tree[1] && tree[1][0] == :lvar && tree[1][1] == :_erbout && tree[2] == :concat #optimisation:call concat on _erbout is safe... dont need to route through _sbm
49
+ add_line_number(tree, s(tree[0], tree[1], tree[2], process(tree[3])))
50
+ elsif tree[1] && tree[2] == :to_s && tree[3][0] == :arglist && tree[3].length == 1 #optimisation:call to_s on an object is safe... dont need to route through _sbm
51
+ add_line_number(tree, s(tree[0], process(tree[1]), tree[2], tree[3]))
52
+ elsif tree[1]
53
+ #rewrite obj.call(arg1, arg2, argN) to obj._invoke_sbm(:call, arg1, arg2, argN)
54
+ args = [:arglist]
55
+ args << s(:lit, tree[2])
56
+ args << s(:ivar, "@_sb_context".intern)
57
+ for i in 1...tree[3].length
58
+ args << process(tree[3][i])
59
+ end
60
+ add_line_number(tree, s(:call, process(tree[1]), :_sbm, args))
61
+ else
62
+ #call on mixed in method or passed in variable
63
+ receiver = s(:self)
64
+ #rewrite local_call(arg1, arg2, argN) to self._get_local(:local_call, arg1, arg2, argN)
65
+ args = [:arglist]
66
+ args << s(:lit, tree[2])
67
+ for i in 1...tree[3].length
68
+ args << tree[3][i]
69
+ end
70
+ add_line_number(tree, s(:call, s(:self), :_get_local, process(args)))
71
+ end
72
+ end
73
+
74
+ #disallowed
75
+
76
+ def process_iasgn(tree)
77
+ puts tree.inspect if $DEBUG
78
+ raise SandboxedErb::CompileSecurityError, "Line #{tree.line}: You cannot assign instance members in a template"
79
+ end
80
+
81
+ def process_ivar(tree)
82
+ puts tree.inspect if $DEBUG
83
+ raise SandboxedErb::CompileSecurityError, "Line #{tree.line}: You cannot access instance members in a template"
84
+ end
85
+
86
+ def process_cvasgn(tree)
87
+ puts tree.inspect if $DEBUG
88
+ raise SandboxedErb::CompileSecurityError, "Line #{tree.line}: You cannot assign class members in a template"
89
+ end
90
+
91
+ def process_cvdecl(tree)
92
+ puts tree.inspect if $DEBUG
93
+ raise SandboxedErb::CompileSecurityError, "Line #{tree.line}: You cannot declare class members in a template"
94
+ end
95
+
96
+ def process_cdecl(tree)
97
+ puts tree.inspect if $DEBUG
98
+ raise SandboxedErb::CompileSecurityError, "Line #{tree.line}: You cannot define a constant in a template"
99
+ end
100
+
101
+ def process_const(tree)
102
+ puts tree.inspect if $DEBUG
103
+ raise SandboxedErb::CompileSecurityError, "Line #{tree.line}: You cannot access a constant in a template"
104
+ end
105
+
106
+ def process_class(tree)
107
+ puts tree.inspect if $DEBUG
108
+ raise SandboxedErb::CompileSecurityError, "Line #{tree.line}: You cannot define a class in a template"
109
+ end
110
+
111
+ def process_module(tree)
112
+ puts tree.inspect if $DEBUG
113
+ raise SandboxedErb::CompileSecurityError, "Line #{tree.line}: You cannot define a module in a template"
114
+ end
115
+
116
+ def process_defn(tree)
117
+ puts tree.inspect if $DEBUG
118
+ raise SandboxedErb::CompileSecurityError, "Line #{tree.line}: You cannot define a method in a template"
119
+ end
120
+
121
+ def process_defs(tree)
122
+ puts tree.inspect if $DEBUG
123
+ raise SandboxedErb::CompileSecurityError, "Line #{tree.line}: You cannot define a method in a template"
124
+ end
125
+
126
+ def process_super(tree)
127
+ puts tree.inspect if $DEBUG
128
+ raise SandboxedErb::CompileSecurityError, "Line #{tree.line}: You cannot call super in a template"
129
+ end
130
+
131
+ def process_gvar(tree)
132
+ puts tree.inspect if $DEBUG
133
+ raise SandboxedErb::CompileSecurityError, "Line #{tree.line}: You cannot access global variables in a template"
134
+ end
135
+
136
+ def process_gasgn(tree)
137
+ puts tree.inspect if $DEBUG
138
+ raise SandboxedErb::CompileSecurityError, "Line #{tree.line}: You cannot assign global variables in a template"
139
+ end
140
+
141
+ def process_xstr(tree)
142
+ puts tree.inspect if $DEBUG
143
+ raise SandboxedErb::CompileSecurityError, "Line #{tree.line}: You cannot make a system call in a template"
144
+ end
145
+
146
+ def fallback_process(tree)
147
+ puts tree.inspect if $DEBUG
148
+ puts "Fallback called" if $DEBUG
149
+ raise SandboxedErb::CompileSecurityError, "Line #{tree.line}: Invalid call used: #{tree[0]}"
150
+ end
151
+
152
+
153
+ #allowed
154
+
155
+ [[:block, true],[:lasgn, true],[:arglist, true],[:str, true],[:dstr, true],[:evstr, true],[:lit, true],[:lvar, true],[:for, true], [:while, true], [:do, true], [:if, true], [:case, true], [:when, true], [:array, true], [:hash, true]].each { |action, add_line_number|
156
+ if add_line_number
157
+ define_method "process_#{action}".intern do |tree|
158
+ puts tree.inspect if $DEBUG
159
+ add_line_number(tree, passthrough(tree))
160
+ end
161
+ else
162
+ define_method "process_#{action}".intern do |tree|
163
+ puts tree.inspect if $DEBUG
164
+ passthrough tree
165
+ end
166
+ end
167
+ }
168
+
169
+
170
+
171
+ private
172
+ #taken from SexpProcessor: just process the tag by processing all nested tags, leaving current tag untouched...
173
+ def passthrough(exp)
174
+ result = self.expected.new
175
+ type = exp.first
176
+ exp_orig = nil
177
+
178
+ in_context type do
179
+ until exp.empty? do
180
+ sub_exp = exp.shift
181
+ sub_result = nil
182
+ if Array === sub_exp then
183
+ sub_result = error_handler(type, exp_orig) do
184
+ process(sub_exp)
185
+ end
186
+ raise "Result is a bad type" unless Array === sub_exp
187
+ raise "Result does not have a type in front: #{sub_exp.inspect}" unless Symbol === sub_exp.first unless sub_exp.empty?
188
+ else
189
+ sub_result = sub_exp
190
+ end
191
+ result << sub_result
192
+ end
193
+
194
+ begin
195
+ result.sexp_type = exp.sexp_type
196
+ rescue Exception
197
+ # nothing to do, on purpose
198
+ end
199
+ end
200
+ result
201
+ end
202
+
203
+ def add_line_number(original_tree, processed_tree)
204
+ if original_tree.respond_to?(:line) && @last_line_number != original_tree.line && !original_tree.line.nil?
205
+ puts "@last_line_number (#{@last_line_number}) != original_tree.line (#{original_tree.line})" if $DEBUG
206
+ @last_line_number = original_tree.line
207
+ s(:block, s(:call, nil, :_sln, s(:arglist, s(:lit, original_tree.line))), processed_tree)
208
+ else
209
+ processed_tree
210
+ end
211
+ end
212
+
213
+ end
214
+ end
215
+