1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 """
22 'Safe' python code evaluation
23
24 Based on the public domain code of Babar K. Zafar
25 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/496746
26 (version 0.1 or 1.2 May 27 2006)
27
28 The idea is to examine the compiled ast tree and chack for invalid
29 entries
30
31 I have removed the timeout checking as this probably isn't a serious
32 problem for veusz documents
33 """
34
35 import parser
36 import inspect, compiler.ast
37 import thread, time
38 import __builtin__
39 import os.path
40
41
42
43
44
45
46
47
48 DEBUG = False
49
50
51 all_ast_nodes = [name for (name, obj) in inspect.getmembers(compiler.ast)
52 if inspect.isclass(obj) and
53 issubclass(obj, compiler.ast.Node)]
54
55
56 all_builtins = [name for (name, obj) in inspect.getmembers(__builtin__)
57 if inspect.isbuiltin(obj) or
58 (inspect.isclass(obj) and not issubclass(obj, Exception))]
59
60
61
62
63
65 return obj.__class__.__name__
66
68 return (node.lineno) and node.lineno or 0
69
70
71
72
73
74
75 unallowed_ast_nodes = (
76
77
78
79 'Backquote',
80
81
82
83
84 'Exec',
85
86
87 'From',
88
89
90
91 'Import',
92
93
94
95
96
97
98 'Raise',
99
100
101 'TryExcept', 'TryFinally',
102
103
104 )
105
106
107 unallowed_builtins = (
108 '__import__',
109
110
111 'compile',
112
113 'delattr',
114
115 'dir',
116
117 'eval', 'execfile', 'file',
118
119 'getattr', 'globals', 'hasattr',
120
121 'input',
122
123
124 'locals',
125
126 'open',
127
128 'raw_input',
129
130 'reload',
131
132 'setattr',
133
134
135 'vars',
136
137 )
138
139
140 for ast_name in unallowed_ast_nodes:
141 assert ast_name in all_ast_nodes
142 for name in unallowed_builtins:
143 assert name in all_builtins
144
145
146 unallowed_ast_nodes = dict( (i, True) for i in unallowed_ast_nodes )
147 unallowed_builtins = dict( (i, True) for i in unallowed_builtins )
148
149
150
151
152
153
154 unallowed_attr = (
155 'im_class', 'im_func', 'im_self',
156 'func_code', 'func_defaults', 'func_globals', 'func_name',
157 'tb_frame', 'tb_next',
158 'f_back', 'f_builtins', 'f_code', 'f_exc_traceback',
159 'f_exc_type', 'f_exc_value', 'f_globals', 'f_locals' )
160 unallowed_attr = dict( (i, True) for i in unallowed_attr )
161
167
168
169
170
171
173 """
174 Base class for all which occur while walking the AST.
175
176 Attributes:
177 errmsg = short decription about the nature of the error
178 lineno = line offset to where error occured in source code
179 """
181 self.errmsg, self.lineno = errmsg, lineno
183 return "line %d : %s" % (self.lineno, self.errmsg)
184
186 "Expression/statement in AST evaluates to a restricted AST node type."
187 pass
189 "Expression/statement in tried to access a restricted builtin."
190 pass
192 "Expression/statement in tried to access a restricted attribute."
193 pass
194
196 """
197 Data-driven visitor which walks the AST for some code and makes
198 sure it doesn't contain any expression/statements which are
199 declared as restricted in 'unallowed_ast_nodes'. We'll also make
200 sure that there aren't any attempts to access/lookup restricted
201 builtin declared in 'unallowed_builtins'. By default we also won't
202 allow access to lowlevel stuff which can be used to dynamically
203 access non-local envrioments.
204
205 Interface:
206 walk(ast) = validate AST and return True if AST is 'safe'
207
208 Attributes:
209 errors = list of SafeEvalError if walk() returned False
210
211 Implementation:
212
213 The visitor will automatically generate methods for all of the
214 available AST node types and redirect them to self.ok or self.fail
215 reflecting the configuration in 'unallowed_ast_nodes'. While
216 walking the AST we simply forward the validating step to each of
217 node callbacks which take care of reporting errors.
218 """
219
230
231 - def walk(self, ast):
232 "Validate each node in AST and return True if AST is 'safe'."
233 self.visit(ast)
234 return self.errors == []
235
236 - def visit(self, node, *args):
237 "Recursively validate node and all of its children."
238 fn = getattr(self, 'visit' + classname(node))
239 if DEBUG: self.trace(node)
240 fn(node, *args)
241 for child in node.getChildNodes():
242 self.visit(child, *args)
243
254
262
263 - def ok(self, node, *args):
264 "Default callback for 'harmless' AST nodes."
265 pass
266
267 - def fail(self, node, *args):
273
275 "Debugging utility for tracing the validation of AST nodes."
276 print classname(node)
277 for attr in dir(node):
278 if attr[:2] != '__':
279 print ' ' * 4, "%-15.15s" % attr, getattr(node, attr)
280
281
282
283
284
285 -def checkContextOkay(context):
286 """Check the context statements will be executed in.
287
288 Returns True if context is okay
289 """
290
291 ctx_errkeys, ctx_errors = [], []
292 for (key, obj) in context.items():
293 if inspect.isbuiltin(obj):
294 ctx_errkeys.append(key)
295 ctx_errors.append("key '%s' : unallowed builtin %s" % (key, obj))
296 if inspect.ismodule(obj):
297 ctx_errkeys.append(key)
298 ctx_errors.append("key '%s' : unallowed module %s" % (key, obj))
299
300 if ctx_errors:
301 raise SafeEvalContextException(ctx_errkeys, ctx_errors)
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
319 """Check code, returning errors (if any) or None if okay"""
320
321 try:
322 ast = compiler.parse(code)
323 except SyntaxError, e:
324 return [e]
325 checker = SafeEvalVisitor()
326
327 if checker.walk(ast):
328 return None
329 else:
330 return checker.errors
331
332
333
334
335
337 "Base class for all safe-eval related errors."
338 pass
339
341 """
342 Exception class for reporting all errors which occured while
343 validating AST for source code in safe_eval().
344
345 Attributes:
346 code = raw source code which failed to validate
347 errors = list of SafeEvalError
348 """
350 self.code, self.errors = code, errors
352 return '\n'.join([str(err) for err in self.errors])
353
354 -class SafeEvalContextException(SafeEvalException):
355 """
356 Exception class for reporting unallowed objects found in the dict
357 intended to be used as the local enviroment in safe_eval().
358
359 Attributes:
360 keys = list of keys of the unallowed objects
361 errors = list of strings describing the nature of the error
362 for each key in 'keys'
363 """
364 - def __init__(self, keys, errors):
365 self.keys, self.errors = keys, errors
367 return '\n'.join([str(err) for err in self.errors])
368
370 """
371 Exception class for reporting that code evaluation execeeded
372 the given timelimit.
373
374 Attributes:
375 timeout = time limit in seconds
376 """
378 self.timeout = timeout
380 return "Timeout limit execeeded (%s secs) during exec" % self.timeout
381
383 """
384 Dynamically execute 'code' using 'context' as the global enviroment.
385 SafeEvalTimeoutException is raised if execution does not finish within
386 the given timelimit.
387 """
388 assert(timeout_secs > 0)
389
390 signal_finished = False
391
392 def alarm(secs):
393 def wait(secs):
394 for n in xrange(timeout_secs):
395 time.sleep(1)
396 if signal_finished: break
397 else:
398 thread.interrupt_main()
399 thread.start_new_thread(wait, (secs,))
400
401 try:
402 alarm(timeout_secs)
403 exec code in context
404 signal_finished = True
405 except KeyboardInterrupt:
406 raise SafeEvalTimeoutException(timeout_secs)
407
409 """
410 Validate source code and make sure it contains no unauthorized
411 expression/statements as configured via 'unallowed_ast_nodes' and
412 'unallowed_builtins'. By default this means that code is not
413 allowed import modules or access dangerous builtins like 'open' or
414 'eval'. If code is considered 'safe' it will be executed via
415 'exec' using 'context' as the global environment. More details on
416 how code is executed can be found in the Python Reference Manual
417 section 6.14 (ignore the remark on '__builtins__'). The 'context'
418 enviroment is also validated and is not allowed to contain modules
419 or builtins. The following exception will be raised on errors:
420
421 if 'context' contains unallowed objects =
422 SafeEvalContextException
423
424 if code is didn't validate and is considered 'unsafe' =
425 SafeEvalCodeException
426
427 if code did not execute within the given timelimit =
428 SafeEvalTimeoutException
429 """
430 ctx_errkeys, ctx_errors = [], []
431 for (key, obj) in context.items():
432 if inspect.isbuiltin(obj):
433 ctx_errkeys.append(key)
434 ctx_errors.append("key '%s' : unallowed builtin %s" % (key, obj))
435 if inspect.ismodule(obj):
436 ctx_errkeys.append(key)
437 ctx_errors.append("key '%s' : unallowed module %s" % (key, obj))
438
439 if ctx_errors:
440 raise SafeEvalContextException(ctx_errkeys, ctx_errors)
441
442 ast = compiler.parse(code)
443 checker = SafeEvalVisitor()
444
445 if checker.walk(ast):
446 exec_timed(code, context, timeout_secs)
447 else:
448 raise SafeEvalCodeException(code, checker.errors)
449
450
451
452
453
454 import unittest
455
461
466
471
476
478
479 def test(): time.sleep(2)
480 env = {'test':test}
481 timed_safe_eval("test()", env, timeout_secs = 5)
482
487
489
490 env = {'f' : __builtins__.open, 'g' : time}
491 self.assertRaises(SafeEvalException, \
492 timed_safe_eval, "print 1", env)
493
495
496 self.value = 0
497 def test(): self.value = 1
498 env = {'test':test}
499 timed_safe_eval("test()", env)
500 self.assertEqual(self.value, 1)
501
502 if __name__ == "__main__":
503 unittest.main()
504
505
506
507
508