1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
|
#!/usr/bin/python
"""Safe user-supplied python expression evaluation."""
import ast
import dataclasses
from typing import Callable
__version__ = '2.0.5'
class EvalException(Exception):
pass
class ValidationException(EvalException):
pass
class CompilationException(EvalException):
exc = None
def __init__(self, exc):
super().__init__(exc)
self.exc = exc
class ExecutionException(EvalException):
exc = None
def __init__(self, exc):
super().__init__(exc)
self.exc = exc
@dataclasses.dataclass
class EvalModel:
""" eval security model """
nodes: list = dataclasses.field(default_factory=list)
allowed_functions: list = dataclasses.field(default_factory=list)
imported_functions: dict = dataclasses.field(default_factory=dict)
attributes: list = dataclasses.field(default_factory=list)
def clone(self):
return EvalModel(**dataclasses.asdict(self))
class SafeAST(ast.NodeVisitor):
"""AST-tree walker class."""
def __init__(self, model: EvalModel):
self.model = model
def generic_visit(self, node):
"""Check node, raise exception if node is not in whitelist."""
if type(node).__name__ in self.model.nodes:
if isinstance(node, ast.Attribute):
if node.attr not in self.model.attributes:
raise ValidationException(
"Attribute {aname} is not allowed".format(
aname=node.attr))
if isinstance(node, ast.Call):
if isinstance(node.func, ast.Name):
if node.func.id not in self.model.allowed_functions and node.func.id not in self.model.imported_functions:
raise ValidationException(
"Call to function {fname}() is not allowed".format(
fname=node.func.id))
else:
# Call to allowed function. good. No exception
pass
elif isinstance(node.func, ast.Attribute):
pass
# print("attr:", node.func.attr)
else:
raise ValidationException('Indirect function call')
ast.NodeVisitor.generic_visit(self, node)
else:
raise ValidationException(
"Node type {optype!r} is not allowed. (whitelist it manually)".format(
optype=type(node).__name__))
base_eval_model = EvalModel(
nodes = [
# 123, 'asdf'
'Num', 'Str',
# any expression or constant
'Expression', 'Constant',
# == ...
'Compare', 'Eq', 'NotEq', 'Gt', 'GtE', 'Lt', 'LtE',
# variable name
'Name', 'Load',
'BinOp',
'Add', 'Sub', 'USub',
'Subscript', 'Index', # person['name']
'BoolOp', 'And', 'Or', 'UnaryOp', 'Not', # True and True
"In", "NotIn", # "aaa" in i['list']
"IfExp", # for if expressions, like: expr1 if expr2 else expr3
"NameConstant", # for True and False constants
"Div", "Mod"
],
)
mult_eval_model = base_eval_model.clone()
mult_eval_model.nodes.append('Mul')
class Expr():
def __init__(self, expr, model=None, filename=None):
self.expr = expr
self.model = model or base_eval_model
try:
self.node = ast.parse(self.expr, '<usercode>', 'eval')
except SyntaxError as e:
raise CompilationException(e)
v = SafeAST(model = self.model)
v.visit(self.node)
self.code = compile(self.node, filename or '<usercode>', 'eval')
def eval(self, ctx=None):
if ctx:
global_ctx = {
**self.model.imported_functions,
**ctx
}
else:
global_ctx = self.model.imported_functions
try:
result = eval(self.code, global_ctx)
except Exception as e:
raise ExecutionException(e)
return result
def __str__(self):
return("Expr(expr={expr!r})".format(expr=self.expr))
|