Projet

Général

Profil

0003-utils-add-module-to-evaluate-condition-expressions-s.patch

Benjamin Dauvergne, 09 août 2019 14:39

Télécharger (9,33 ko)

Voir les différences:

Subject: [PATCH 3/4] utils: add module to evaluate condition expressions
 safely (#35302)

 src/authentic2/utils/evaluate.py | 182 +++++++++++++++++++++++++++++++
 tests/test_utils_evaluate.py     |  73 +++++++++++++
 2 files changed, 255 insertions(+)
 create mode 100644 src/authentic2/utils/evaluate.py
 create mode 100644 tests/test_utils_evaluate.py
src/authentic2/utils/evaluate.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17
import sys
18

  
19
from django.core.exceptions import ValidationError
20
try:
21
    from functools import lru_cache
22
except ImportError:
23
    from django.utils.lru_cache import lru_cache
24
from django.utils.translation import ugettext as _
25
from django.utils import six
26

  
27

  
28
import ast
29

  
30

  
31
class Unparse(ast.NodeVisitor):
32
    def visit_Name(self, node):
33
        return node.id
34

  
35

  
36
class ExpressionError(ValidationError):
37
    colummn = None
38
    node = None
39
    text = None
40

  
41
    def __init__(self, message, code=None, params=None, node=None, column=None, text=None):
42
        super(ExpressionError, self).__init__(message, code=code, params=params)
43
        if hasattr(node, 'col_offset'):
44
            self.set_node(node)
45
        if column is not None:
46
            self.column = column
47
        if text is not None:
48
            self.text = text
49

  
50
    def set_node(self, node):
51
        assert hasattr(node, 'col_offset'), 'only node with col_offset attribute'
52
        self.node = node
53
        self.column = node.col_offset
54
        self.text = Unparse().visit(node)
55

  
56

  
57
class BaseExpressionValidator(ast.NodeVisitor):
58
    authorized_nodes = []
59
    forbidden_nodes = []
60

  
61
    def __init__(self, authorized_nodes=None, forbidden_nodes=None):
62
        if authorized_nodes is not None:
63
            self.authorized_nodes = authorized_nodes
64
        if forbidden_nodes is not None:
65
            self.forbidden_nodes = forbidden_nodes
66

  
67
    def generic_visit(self, node):
68
        # generic node class checks
69
        ok = False
70
        if not isinstance(node, ast.Expression):
71
            for klass in self.authorized_nodes:
72
                if isinstance(node, klass):
73
                    ok = True
74
                    break
75
            for klass in self.forbidden_nodes:
76
                if isinstance(node, klass):
77
                    ok = False
78
        else:
79
            ok = True
80
        if not ok:
81
            raise ExpressionError(_('expression is forbidden'), node=node, code='forbidden-expression')
82

  
83
        # specific node class check
84
        node_name = node.__class__.__name__
85
        check_method = getattr(self, 'check_' + node_name, None)
86
        if check_method:
87
            check_method(node)
88

  
89
        # now recurse on subnodes
90
        try:
91
            return super(BaseExpressionValidator, self).generic_visit(node)
92
        except ExpressionError as e:
93
            # for errors in non expr nodes (so without a col_offset attribute,
94
            # set the nearer expr node as the node of the error
95
            if e.node is None and hasattr(node, 'col_offset'):
96
                e.set_node(node)
97
            six.reraise(*sys.exc_info())
98

  
99
    @lru_cache(maxsize=1024)
100
    def __call__(self, expression):
101
        try:
102
            tree = ast.parse(expression, mode='eval')
103
        except SyntaxError as e:
104
            raise ExpressionError(_('could not parse expression') % e,
105
                                  code='parsing-error',
106
                                  column=e.offset,
107
                                  text=expression)
108
        try:
109
            self.visit(tree)
110
        except ExpressionError as e:
111
            if e.text is None:
112
                e.text = expression
113
            six.reraise(*sys.exc_info())
114
        return compile(tree, expression, mode='eval')
115

  
116

  
117
class ConditionValidator(BaseExpressionValidator):
118
    '''
119
       Only authorize :
120
       - direct variable references, without underscore in them,
121
       - num and str constants,
122
       - boolean expressions with all operators,
123
       - unary operator expressions with all operators,
124
       - if expressions (x if y else z),
125
       - compare expressions with all operators.
126

  
127
       Are implicitely forbidden:
128
       - binary expressions (so no "'aaa' * 99999999999" or 233333333333333233**2232323233232323 bombs),
129
       - lambda,
130
       - literal list, tuple, dict and sets,
131
       - comprehensions (list, dict and set),
132
       - generators,
133
       - yield,
134
       - call,
135
       - Repr node (i dunno what it is),
136
       - attribute access,
137
       - subscript.
138
    '''
139
    authorized_nodes = [
140
        ast.Load,
141
        ast.Name,
142
        ast.Num,
143
        ast.Str,
144
        ast.BoolOp,
145
        ast.UnaryOp,
146
        ast.IfExp,
147
        ast.boolop,
148
        ast.cmpop,
149
        ast.Compare,
150
    ]
151

  
152
    def check_Name(self, node):
153
        if node.id.startswith('_'):
154
            raise ExpressionError(_('name must not start with a _'), code='invalid-variable', node=node)
155

  
156

  
157
validate_condition = ConditionValidator()
158

  
159
condition_safe_globals = {
160
    '__builtins__': {
161
        'True': True,
162
        'False': False,
163
    }
164
}
165

  
166

  
167
def evaluate_condition(expression, ctx=None, validator=None, on_raise=None):
168
    try:
169
        code = (validator or validate_condition)(expression)
170
        try:
171
            return eval(code, condition_safe_globals, ctx or {})
172
        except NameError as e:
173
            # NameError does not report the column of the name reference :/
174
            raise ExpressionError(
175
                _('variable is not defined: %s') % e,
176
                code='undefined-variable',
177
                text=expression,
178
                column=0)
179
    except Exception:
180
        if on_raise is not None:
181
            return on_raise
182
        six.reraise(*sys.exc_info())
tests/test_utils_evaluate.py
1
# authentic2 - versatile identity manager
2
# Copyright (C) 2010-2019 Entr'ouvert
3
#
4
# This program is free software: you can redistribute it and/or modify it
5
# under the terms of the GNU Affero General Public License as published
6
# by the Free Software Foundation, either version 3 of the License, or
7
# (at your option) any later version.
8
#
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
# GNU Affero General Public License for more details.
13
#
14
# You should have received a copy of the GNU Affero General Public License
15
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
16

  
17

  
18
import ast
19

  
20
import pytest
21

  
22
from authentic2.utils.evaluate import (
23
    BaseExpressionValidator, ConditionValidator, ExpressionError,
24
    evaluate_condition)
25

  
26

  
27
def test_base():
28
    v = BaseExpressionValidator()
29

  
30
#    assert v('1')[0] is False
31
#    assert v('\'a\'')[0] is False
32
#    assert v('x')[0] is False
33

  
34
    v = BaseExpressionValidator(authorized_nodes=[ast.Num, ast.Str])
35

  
36
    assert v('1')
37
    assert v('\'a\'')
38

  
39
    # code object is cached
40
    assert v('1') is v('1')
41
    assert v('\'a\'') is v('\'a\'')
42
    with pytest.raises(ExpressionError):
43
        assert v('x')
44

  
45

  
46
def test_condition_validator():
47
    v = ConditionValidator()
48
    assert v('x < 2 and y == \'u\' or \'a\' in z')
49
    with pytest.raises(ExpressionError) as raised:
50
        v('a and _b')
51
    assert raised.value.code == 'invalid-variable'
52
    assert raised.value.text == '_b'
53

  
54
    with pytest.raises(ExpressionError) as raised:
55
        v('a + b')
56

  
57
    with pytest.raises(ExpressionError) as raised:
58
        v('1 + 2')
59

  
60

  
61
def test_evaluate_condition():
62
    v = ConditionValidator()
63

  
64
    assert evaluate_condition('False', validator=v) is False
65
    assert evaluate_condition('True', validator=v) is True
66
    assert evaluate_condition('True and False', validator=v) is False
67
    assert evaluate_condition('True or False', validator=v) is True
68
    assert evaluate_condition('a or b', ctx=dict(a=True, b=False), validator=v) is True
69
    assert evaluate_condition('a < 1', ctx=dict(a=0), validator=v) is True
70
    with pytest.raises(ExpressionError) as exc_info:
71
        evaluate_condition('a < 1', validator=v)
72
    assert exc_info.value.code == 'undefined-variable'
73
    assert evaluate_condition('a < 1', validator=v, on_raise=False) is False
0
-