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