File: __init__.py

package info (click to toggle)
python-evalidate 2.0.5-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 144 kB
  • sloc: python: 500; makefile: 3
file content (144 lines) | stat: -rwxr-xr-x 4,308 bytes parent folder | download
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))