##// END OF EJS Templates
Code in docstring
krassowski -
Show More
@@ -1,746 +1,745 b''
1 from inspect import signature
1 from inspect import signature
2 from typing import (
2 from typing import (
3 Any,
3 Any,
4 Callable,
4 Callable,
5 Dict,
5 Dict,
6 Set,
6 Set,
7 Sequence,
7 Sequence,
8 Tuple,
8 Tuple,
9 NamedTuple,
9 NamedTuple,
10 Type,
10 Type,
11 Literal,
11 Literal,
12 Union,
12 Union,
13 TYPE_CHECKING,
13 TYPE_CHECKING,
14 )
14 )
15 import ast
15 import ast
16 import builtins
16 import builtins
17 import collections
17 import collections
18 import operator
18 import operator
19 import sys
19 import sys
20 from functools import cached_property
20 from functools import cached_property
21 from dataclasses import dataclass, field
21 from dataclasses import dataclass, field
22 from types import MethodDescriptorType, ModuleType
22 from types import MethodDescriptorType, ModuleType
23
23
24 from IPython.utils.docs import GENERATING_DOCUMENTATION
24 from IPython.utils.docs import GENERATING_DOCUMENTATION
25 from IPython.utils.decorators import undoc
25 from IPython.utils.decorators import undoc
26
26
27
27
28 if TYPE_CHECKING or GENERATING_DOCUMENTATION:
28 if TYPE_CHECKING or GENERATING_DOCUMENTATION:
29 from typing_extensions import Protocol
29 from typing_extensions import Protocol
30 else:
30 else:
31 # do not require on runtime
31 # do not require on runtime
32 Protocol = object # requires Python >=3.8
32 Protocol = object # requires Python >=3.8
33
33
34
34
35 @undoc
35 @undoc
36 class HasGetItem(Protocol):
36 class HasGetItem(Protocol):
37 def __getitem__(self, key) -> None:
37 def __getitem__(self, key) -> None:
38 ...
38 ...
39
39
40
40
41 @undoc
41 @undoc
42 class InstancesHaveGetItem(Protocol):
42 class InstancesHaveGetItem(Protocol):
43 def __call__(self, *args, **kwargs) -> HasGetItem:
43 def __call__(self, *args, **kwargs) -> HasGetItem:
44 ...
44 ...
45
45
46
46
47 @undoc
47 @undoc
48 class HasGetAttr(Protocol):
48 class HasGetAttr(Protocol):
49 def __getattr__(self, key) -> None:
49 def __getattr__(self, key) -> None:
50 ...
50 ...
51
51
52
52
53 @undoc
53 @undoc
54 class DoesNotHaveGetAttr(Protocol):
54 class DoesNotHaveGetAttr(Protocol):
55 pass
55 pass
56
56
57
57
58 # By default `__getattr__` is not explicitly implemented on most objects
58 # By default `__getattr__` is not explicitly implemented on most objects
59 MayHaveGetattr = Union[HasGetAttr, DoesNotHaveGetAttr]
59 MayHaveGetattr = Union[HasGetAttr, DoesNotHaveGetAttr]
60
60
61
61
62 def _unbind_method(func: Callable) -> Union[Callable, None]:
62 def _unbind_method(func: Callable) -> Union[Callable, None]:
63 """Get unbound method for given bound method.
63 """Get unbound method for given bound method.
64
64
65 Returns None if cannot get unbound method, or method is already unbound.
65 Returns None if cannot get unbound method, or method is already unbound.
66 """
66 """
67 owner = getattr(func, "__self__", None)
67 owner = getattr(func, "__self__", None)
68 owner_class = type(owner)
68 owner_class = type(owner)
69 name = getattr(func, "__name__", None)
69 name = getattr(func, "__name__", None)
70 instance_dict_overrides = getattr(owner, "__dict__", None)
70 instance_dict_overrides = getattr(owner, "__dict__", None)
71 if (
71 if (
72 owner is not None
72 owner is not None
73 and name
73 and name
74 and (
74 and (
75 not instance_dict_overrides
75 not instance_dict_overrides
76 or (instance_dict_overrides and name not in instance_dict_overrides)
76 or (instance_dict_overrides and name not in instance_dict_overrides)
77 )
77 )
78 ):
78 ):
79 return getattr(owner_class, name)
79 return getattr(owner_class, name)
80 return None
80 return None
81
81
82
82
83 @undoc
83 @undoc
84 @dataclass
84 @dataclass
85 class EvaluationPolicy:
85 class EvaluationPolicy:
86 """Definition of evaluation policy."""
86 """Definition of evaluation policy."""
87
87
88 allow_locals_access: bool = False
88 allow_locals_access: bool = False
89 allow_globals_access: bool = False
89 allow_globals_access: bool = False
90 allow_item_access: bool = False
90 allow_item_access: bool = False
91 allow_attr_access: bool = False
91 allow_attr_access: bool = False
92 allow_builtins_access: bool = False
92 allow_builtins_access: bool = False
93 allow_all_operations: bool = False
93 allow_all_operations: bool = False
94 allow_any_calls: bool = False
94 allow_any_calls: bool = False
95 allowed_calls: Set[Callable] = field(default_factory=set)
95 allowed_calls: Set[Callable] = field(default_factory=set)
96
96
97 def can_get_item(self, value, item):
97 def can_get_item(self, value, item):
98 return self.allow_item_access
98 return self.allow_item_access
99
99
100 def can_get_attr(self, value, attr):
100 def can_get_attr(self, value, attr):
101 return self.allow_attr_access
101 return self.allow_attr_access
102
102
103 def can_operate(self, dunders: Tuple[str, ...], a, b=None):
103 def can_operate(self, dunders: Tuple[str, ...], a, b=None):
104 if self.allow_all_operations:
104 if self.allow_all_operations:
105 return True
105 return True
106
106
107 def can_call(self, func):
107 def can_call(self, func):
108 if self.allow_any_calls:
108 if self.allow_any_calls:
109 return True
109 return True
110
110
111 if func in self.allowed_calls:
111 if func in self.allowed_calls:
112 return True
112 return True
113
113
114 owner_method = _unbind_method(func)
114 owner_method = _unbind_method(func)
115
115
116 if owner_method and owner_method in self.allowed_calls:
116 if owner_method and owner_method in self.allowed_calls:
117 return True
117 return True
118
118
119
119
120 def _get_external(module_name: str, access_path: Sequence[str]):
120 def _get_external(module_name: str, access_path: Sequence[str]):
121 """Get value from external module given a dotted access path.
121 """Get value from external module given a dotted access path.
122
122
123 Raises:
123 Raises:
124 * `KeyError` if module is removed not found, and
124 * `KeyError` if module is removed not found, and
125 * `AttributeError` if acess path does not match an exported object
125 * `AttributeError` if acess path does not match an exported object
126 """
126 """
127 member_type = sys.modules[module_name]
127 member_type = sys.modules[module_name]
128 for attr in access_path:
128 for attr in access_path:
129 member_type = getattr(member_type, attr)
129 member_type = getattr(member_type, attr)
130 return member_type
130 return member_type
131
131
132
132
133 def _has_original_dunder_external(
133 def _has_original_dunder_external(
134 value,
134 value,
135 module_name: str,
135 module_name: str,
136 access_path: Sequence[str],
136 access_path: Sequence[str],
137 method_name: str,
137 method_name: str,
138 ):
138 ):
139 if module_name not in sys.modules:
139 if module_name not in sys.modules:
140 # LBYLB as it is faster
140 # LBYLB as it is faster
141 return False
141 return False
142 try:
142 try:
143 member_type = _get_external(module_name, access_path)
143 member_type = _get_external(module_name, access_path)
144 value_type = type(value)
144 value_type = type(value)
145 if type(value) == member_type:
145 if type(value) == member_type:
146 return True
146 return True
147 if method_name == "__getattribute__":
147 if method_name == "__getattribute__":
148 # we have to short-circuit here due to an unresolved issue in
148 # we have to short-circuit here due to an unresolved issue in
149 # `isinstance` implementation: https://bugs.python.org/issue32683
149 # `isinstance` implementation: https://bugs.python.org/issue32683
150 return False
150 return False
151 if isinstance(value, member_type):
151 if isinstance(value, member_type):
152 method = getattr(value_type, method_name, None)
152 method = getattr(value_type, method_name, None)
153 member_method = getattr(member_type, method_name, None)
153 member_method = getattr(member_type, method_name, None)
154 if member_method == method:
154 if member_method == method:
155 return True
155 return True
156 except (AttributeError, KeyError):
156 except (AttributeError, KeyError):
157 return False
157 return False
158
158
159
159
160 def _has_original_dunder(
160 def _has_original_dunder(
161 value, allowed_types, allowed_methods, allowed_external, method_name
161 value, allowed_types, allowed_methods, allowed_external, method_name
162 ):
162 ):
163 # note: Python ignores `__getattr__`/`__getitem__` on instances,
163 # note: Python ignores `__getattr__`/`__getitem__` on instances,
164 # we only need to check at class level
164 # we only need to check at class level
165 value_type = type(value)
165 value_type = type(value)
166
166
167 # strict type check passes β†’ no need to check method
167 # strict type check passes β†’ no need to check method
168 if value_type in allowed_types:
168 if value_type in allowed_types:
169 return True
169 return True
170
170
171 method = getattr(value_type, method_name, None)
171 method = getattr(value_type, method_name, None)
172
172
173 if method is None:
173 if method is None:
174 return None
174 return None
175
175
176 if method in allowed_methods:
176 if method in allowed_methods:
177 return True
177 return True
178
178
179 for module_name, *access_path in allowed_external:
179 for module_name, *access_path in allowed_external:
180 if _has_original_dunder_external(value, module_name, access_path, method_name):
180 if _has_original_dunder_external(value, module_name, access_path, method_name):
181 return True
181 return True
182
182
183 return False
183 return False
184
184
185
185
186 @undoc
186 @undoc
187 @dataclass
187 @dataclass
188 class SelectivePolicy(EvaluationPolicy):
188 class SelectivePolicy(EvaluationPolicy):
189 allowed_getitem: Set[InstancesHaveGetItem] = field(default_factory=set)
189 allowed_getitem: Set[InstancesHaveGetItem] = field(default_factory=set)
190 allowed_getitem_external: Set[Tuple[str, ...]] = field(default_factory=set)
190 allowed_getitem_external: Set[Tuple[str, ...]] = field(default_factory=set)
191
191
192 allowed_getattr: Set[MayHaveGetattr] = field(default_factory=set)
192 allowed_getattr: Set[MayHaveGetattr] = field(default_factory=set)
193 allowed_getattr_external: Set[Tuple[str, ...]] = field(default_factory=set)
193 allowed_getattr_external: Set[Tuple[str, ...]] = field(default_factory=set)
194
194
195 allowed_operations: Set = field(default_factory=set)
195 allowed_operations: Set = field(default_factory=set)
196 allowed_operations_external: Set[Tuple[str, ...]] = field(default_factory=set)
196 allowed_operations_external: Set[Tuple[str, ...]] = field(default_factory=set)
197
197
198 _operation_methods_cache: Dict[str, Set[Callable]] = field(
198 _operation_methods_cache: Dict[str, Set[Callable]] = field(
199 default_factory=dict, init=False
199 default_factory=dict, init=False
200 )
200 )
201
201
202 def can_get_attr(self, value, attr):
202 def can_get_attr(self, value, attr):
203 has_original_attribute = _has_original_dunder(
203 has_original_attribute = _has_original_dunder(
204 value,
204 value,
205 allowed_types=self.allowed_getattr,
205 allowed_types=self.allowed_getattr,
206 allowed_methods=self._getattribute_methods,
206 allowed_methods=self._getattribute_methods,
207 allowed_external=self.allowed_getattr_external,
207 allowed_external=self.allowed_getattr_external,
208 method_name="__getattribute__",
208 method_name="__getattribute__",
209 )
209 )
210 has_original_attr = _has_original_dunder(
210 has_original_attr = _has_original_dunder(
211 value,
211 value,
212 allowed_types=self.allowed_getattr,
212 allowed_types=self.allowed_getattr,
213 allowed_methods=self._getattr_methods,
213 allowed_methods=self._getattr_methods,
214 allowed_external=self.allowed_getattr_external,
214 allowed_external=self.allowed_getattr_external,
215 method_name="__getattr__",
215 method_name="__getattr__",
216 )
216 )
217
217
218 accept = False
218 accept = False
219
219
220 # Many objects do not have `__getattr__`, this is fine.
220 # Many objects do not have `__getattr__`, this is fine.
221 if has_original_attr is None and has_original_attribute:
221 if has_original_attr is None and has_original_attribute:
222 accept = True
222 accept = True
223 else:
223 else:
224 # Accept objects without modifications to `__getattr__` and `__getattribute__`
224 # Accept objects without modifications to `__getattr__` and `__getattribute__`
225 accept = has_original_attr and has_original_attribute
225 accept = has_original_attr and has_original_attribute
226
226
227 if accept:
227 if accept:
228 # We still need to check for overriden properties.
228 # We still need to check for overriden properties.
229
229
230 value_class = type(value)
230 value_class = type(value)
231 if not hasattr(value_class, attr):
231 if not hasattr(value_class, attr):
232 return True
232 return True
233
233
234 class_attr_val = getattr(value_class, attr)
234 class_attr_val = getattr(value_class, attr)
235 is_property = isinstance(class_attr_val, property)
235 is_property = isinstance(class_attr_val, property)
236
236
237 if not is_property:
237 if not is_property:
238 return True
238 return True
239
239
240 # Properties in allowed types are ok (although we do not include any
240 # Properties in allowed types are ok (although we do not include any
241 # properties in our default allow list currently).
241 # properties in our default allow list currently).
242 if type(value) in self.allowed_getattr:
242 if type(value) in self.allowed_getattr:
243 return True # pragma: no cover
243 return True # pragma: no cover
244
244
245 # Properties in subclasses of allowed types may be ok if not changed
245 # Properties in subclasses of allowed types may be ok if not changed
246 for module_name, *access_path in self.allowed_getattr_external:
246 for module_name, *access_path in self.allowed_getattr_external:
247 try:
247 try:
248 external_class = _get_external(module_name, access_path)
248 external_class = _get_external(module_name, access_path)
249 external_class_attr_val = getattr(external_class, attr)
249 external_class_attr_val = getattr(external_class, attr)
250 except (KeyError, AttributeError):
250 except (KeyError, AttributeError):
251 return False # pragma: no cover
251 return False # pragma: no cover
252 return class_attr_val == external_class_attr_val
252 return class_attr_val == external_class_attr_val
253
253
254 return False
254 return False
255
255
256 def can_get_item(self, value, item):
256 def can_get_item(self, value, item):
257 """Allow accessing `__getiitem__` of allow-listed instances unless it was not modified."""
257 """Allow accessing `__getiitem__` of allow-listed instances unless it was not modified."""
258 return _has_original_dunder(
258 return _has_original_dunder(
259 value,
259 value,
260 allowed_types=self.allowed_getitem,
260 allowed_types=self.allowed_getitem,
261 allowed_methods=self._getitem_methods,
261 allowed_methods=self._getitem_methods,
262 allowed_external=self.allowed_getitem_external,
262 allowed_external=self.allowed_getitem_external,
263 method_name="__getitem__",
263 method_name="__getitem__",
264 )
264 )
265
265
266 def can_operate(self, dunders: Tuple[str, ...], a, b=None):
266 def can_operate(self, dunders: Tuple[str, ...], a, b=None):
267 objects = [a]
267 objects = [a]
268 if b is not None:
268 if b is not None:
269 objects.append(b)
269 objects.append(b)
270 return all(
270 return all(
271 [
271 [
272 _has_original_dunder(
272 _has_original_dunder(
273 obj,
273 obj,
274 allowed_types=self.allowed_operations,
274 allowed_types=self.allowed_operations,
275 allowed_methods=self._operator_dunder_methods(dunder),
275 allowed_methods=self._operator_dunder_methods(dunder),
276 allowed_external=self.allowed_operations_external,
276 allowed_external=self.allowed_operations_external,
277 method_name=dunder,
277 method_name=dunder,
278 )
278 )
279 for dunder in dunders
279 for dunder in dunders
280 for obj in objects
280 for obj in objects
281 ]
281 ]
282 )
282 )
283
283
284 def _operator_dunder_methods(self, dunder: str) -> Set[Callable]:
284 def _operator_dunder_methods(self, dunder: str) -> Set[Callable]:
285 if dunder not in self._operation_methods_cache:
285 if dunder not in self._operation_methods_cache:
286 self._operation_methods_cache[dunder] = self._safe_get_methods(
286 self._operation_methods_cache[dunder] = self._safe_get_methods(
287 self.allowed_operations, dunder
287 self.allowed_operations, dunder
288 )
288 )
289 return self._operation_methods_cache[dunder]
289 return self._operation_methods_cache[dunder]
290
290
291 @cached_property
291 @cached_property
292 def _getitem_methods(self) -> Set[Callable]:
292 def _getitem_methods(self) -> Set[Callable]:
293 return self._safe_get_methods(self.allowed_getitem, "__getitem__")
293 return self._safe_get_methods(self.allowed_getitem, "__getitem__")
294
294
295 @cached_property
295 @cached_property
296 def _getattr_methods(self) -> Set[Callable]:
296 def _getattr_methods(self) -> Set[Callable]:
297 return self._safe_get_methods(self.allowed_getattr, "__getattr__")
297 return self._safe_get_methods(self.allowed_getattr, "__getattr__")
298
298
299 @cached_property
299 @cached_property
300 def _getattribute_methods(self) -> Set[Callable]:
300 def _getattribute_methods(self) -> Set[Callable]:
301 return self._safe_get_methods(self.allowed_getattr, "__getattribute__")
301 return self._safe_get_methods(self.allowed_getattr, "__getattribute__")
302
302
303 def _safe_get_methods(self, classes, name) -> Set[Callable]:
303 def _safe_get_methods(self, classes, name) -> Set[Callable]:
304 return {
304 return {
305 method
305 method
306 for class_ in classes
306 for class_ in classes
307 for method in [getattr(class_, name, None)]
307 for method in [getattr(class_, name, None)]
308 if method
308 if method
309 }
309 }
310
310
311
311
312 class _DummyNamedTuple(NamedTuple):
312 class _DummyNamedTuple(NamedTuple):
313 """Used internally to retrieve methods of named tuple instance."""
313 """Used internally to retrieve methods of named tuple instance."""
314
314
315
315
316 class EvaluationContext(NamedTuple):
316 class EvaluationContext(NamedTuple):
317 #: Local namespace
317 #: Local namespace
318 locals: dict
318 locals: dict
319 #: Global namespace
319 #: Global namespace
320 globals: dict
320 globals: dict
321 #: Evaluation policy identifier
321 #: Evaluation policy identifier
322 evaluation: Literal[
322 evaluation: Literal[
323 "forbidden", "minimal", "limited", "unsafe", "dangerous"
323 "forbidden", "minimal", "limited", "unsafe", "dangerous"
324 ] = "forbidden"
324 ] = "forbidden"
325 #: Whether the evalution of code takes place inside of a subscript.
325 #: Whether the evalution of code takes place inside of a subscript.
326 #: Useful for evaluating ``:-1, 'col'`` in ``df[:-1, 'col']``.
326 #: Useful for evaluating ``:-1, 'col'`` in ``df[:-1, 'col']``.
327 in_subscript: bool = False
327 in_subscript: bool = False
328
328
329
329
330 class _IdentitySubscript:
330 class _IdentitySubscript:
331 """Returns the key itself when item is requested via subscript."""
331 """Returns the key itself when item is requested via subscript."""
332
332
333 def __getitem__(self, key):
333 def __getitem__(self, key):
334 return key
334 return key
335
335
336
336
337 IDENTITY_SUBSCRIPT = _IdentitySubscript()
337 IDENTITY_SUBSCRIPT = _IdentitySubscript()
338 SUBSCRIPT_MARKER = "__SUBSCRIPT_SENTINEL__"
338 SUBSCRIPT_MARKER = "__SUBSCRIPT_SENTINEL__"
339
339
340
340
341 class GuardRejection(Exception):
341 class GuardRejection(Exception):
342 """Exception raised when guard rejects evaluation attempt."""
342 """Exception raised when guard rejects evaluation attempt."""
343
343
344 pass
344 pass
345
345
346
346
347 def guarded_eval(code: str, context: EvaluationContext):
347 def guarded_eval(code: str, context: EvaluationContext):
348 """Evaluate provided code in the evaluation context.
348 """Evaluate provided code in the evaluation context.
349
349
350 If evaluation policy given by context is set to ``forbidden``
350 If evaluation policy given by context is set to ``forbidden``
351 no evaluation will be performed; if it is set to ``dangerous``
351 no evaluation will be performed; if it is set to ``dangerous``
352 standard :func:`eval` will be used; finally, for any other,
352 standard :func:`eval` will be used; finally, for any other,
353 policy :func:`eval_node` will be called on parsed AST.
353 policy :func:`eval_node` will be called on parsed AST.
354 """
354 """
355 locals_ = context.locals
355 locals_ = context.locals
356
356
357 if context.evaluation == "forbidden":
357 if context.evaluation == "forbidden":
358 raise GuardRejection("Forbidden mode")
358 raise GuardRejection("Forbidden mode")
359
359
360 # note: not using `ast.literal_eval` as it does not implement
360 # note: not using `ast.literal_eval` as it does not implement
361 # getitem at all, for example it fails on simple `[0][1]`
361 # getitem at all, for example it fails on simple `[0][1]`
362
362
363 if context.in_subscript:
363 if context.in_subscript:
364 # syntatic sugar for ellipsis (:) is only available in susbcripts
364 # syntatic sugar for ellipsis (:) is only available in susbcripts
365 # so we need to trick the ast parser into thinking that we have
365 # so we need to trick the ast parser into thinking that we have
366 # a subscript, but we need to be able to later recognise that we did
366 # a subscript, but we need to be able to later recognise that we did
367 # it so we can ignore the actual __getitem__ operation
367 # it so we can ignore the actual __getitem__ operation
368 if not code:
368 if not code:
369 return tuple()
369 return tuple()
370 locals_ = locals_.copy()
370 locals_ = locals_.copy()
371 locals_[SUBSCRIPT_MARKER] = IDENTITY_SUBSCRIPT
371 locals_[SUBSCRIPT_MARKER] = IDENTITY_SUBSCRIPT
372 code = SUBSCRIPT_MARKER + "[" + code + "]"
372 code = SUBSCRIPT_MARKER + "[" + code + "]"
373 context = EvaluationContext(**{**context._asdict(), **{"locals": locals_}})
373 context = EvaluationContext(**{**context._asdict(), **{"locals": locals_}})
374
374
375 if context.evaluation == "dangerous":
375 if context.evaluation == "dangerous":
376 return eval(code, context.globals, context.locals)
376 return eval(code, context.globals, context.locals)
377
377
378 expression = ast.parse(code, mode="eval")
378 expression = ast.parse(code, mode="eval")
379
379
380 return eval_node(expression, context)
380 return eval_node(expression, context)
381
381
382
382
383 BINARY_OP_DUNDERS: Dict[Type[ast.operator], Tuple[str]] = {
383 BINARY_OP_DUNDERS: Dict[Type[ast.operator], Tuple[str]] = {
384 ast.Add: ("__add__",),
384 ast.Add: ("__add__",),
385 ast.Sub: ("__sub__",),
385 ast.Sub: ("__sub__",),
386 ast.Mult: ("__mul__",),
386 ast.Mult: ("__mul__",),
387 ast.Div: ("__truediv__",),
387 ast.Div: ("__truediv__",),
388 ast.FloorDiv: ("__floordiv__",),
388 ast.FloorDiv: ("__floordiv__",),
389 ast.Mod: ("__mod__",),
389 ast.Mod: ("__mod__",),
390 ast.Pow: ("__pow__",),
390 ast.Pow: ("__pow__",),
391 ast.LShift: ("__lshift__",),
391 ast.LShift: ("__lshift__",),
392 ast.RShift: ("__rshift__",),
392 ast.RShift: ("__rshift__",),
393 ast.BitOr: ("__or__",),
393 ast.BitOr: ("__or__",),
394 ast.BitXor: ("__xor__",),
394 ast.BitXor: ("__xor__",),
395 ast.BitAnd: ("__and__",),
395 ast.BitAnd: ("__and__",),
396 ast.MatMult: ("__matmul__",),
396 ast.MatMult: ("__matmul__",),
397 }
397 }
398
398
399 COMP_OP_DUNDERS: Dict[Type[ast.cmpop], Tuple[str, ...]] = {
399 COMP_OP_DUNDERS: Dict[Type[ast.cmpop], Tuple[str, ...]] = {
400 ast.Eq: ("__eq__",),
400 ast.Eq: ("__eq__",),
401 ast.NotEq: ("__ne__", "__eq__"),
401 ast.NotEq: ("__ne__", "__eq__"),
402 ast.Lt: ("__lt__", "__gt__"),
402 ast.Lt: ("__lt__", "__gt__"),
403 ast.LtE: ("__le__", "__ge__"),
403 ast.LtE: ("__le__", "__ge__"),
404 ast.Gt: ("__gt__", "__lt__"),
404 ast.Gt: ("__gt__", "__lt__"),
405 ast.GtE: ("__ge__", "__le__"),
405 ast.GtE: ("__ge__", "__le__"),
406 ast.In: ("__contains__",),
406 ast.In: ("__contains__",),
407 # Note: ast.Is, ast.IsNot, ast.NotIn are handled specially
407 # Note: ast.Is, ast.IsNot, ast.NotIn are handled specially
408 }
408 }
409
409
410 UNARY_OP_DUNDERS: Dict[Type[ast.unaryop], Tuple[str, ...]] = {
410 UNARY_OP_DUNDERS: Dict[Type[ast.unaryop], Tuple[str, ...]] = {
411 ast.USub: ("__neg__",),
411 ast.USub: ("__neg__",),
412 ast.UAdd: ("__pos__",),
412 ast.UAdd: ("__pos__",),
413 # we have to check both __inv__ and __invert__!
413 # we have to check both __inv__ and __invert__!
414 ast.Invert: ("__invert__", "__inv__"),
414 ast.Invert: ("__invert__", "__inv__"),
415 ast.Not: ("__not__",),
415 ast.Not: ("__not__",),
416 }
416 }
417
417
418
418
419 class Duck:
419 class Duck:
420 """A dummy class used to create objects of other classes without calling their __init__"""
420 """A dummy class used to create objects of other classes without calling their ``__init__``"""
421 pass
422
421
423
422
424 def _find_dunder(node_op, dunders) -> Union[Tuple[str, ...], None]:
423 def _find_dunder(node_op, dunders) -> Union[Tuple[str, ...], None]:
425 dunder = None
424 dunder = None
426 for op, candidate_dunder in dunders.items():
425 for op, candidate_dunder in dunders.items():
427 if isinstance(node_op, op):
426 if isinstance(node_op, op):
428 dunder = candidate_dunder
427 dunder = candidate_dunder
429 return dunder
428 return dunder
430
429
431
430
432 def eval_node(node: Union[ast.AST, None], context: EvaluationContext):
431 def eval_node(node: Union[ast.AST, None], context: EvaluationContext):
433 """Evaluate AST node in provided context.
432 """Evaluate AST node in provided context.
434
433
435 Applies evaluation restrictions defined in the context. Currently does not support evaluation of functions with keyword arguments.
434 Applies evaluation restrictions defined in the context. Currently does not support evaluation of functions with keyword arguments.
436
435
437 Does not evaluate actions that always have side effects:
436 Does not evaluate actions that always have side effects:
438
437
439 - class definitions (``class sth: ...``)
438 - class definitions (``class sth: ...``)
440 - function definitions (``def sth: ...``)
439 - function definitions (``def sth: ...``)
441 - variable assignments (``x = 1``)
440 - variable assignments (``x = 1``)
442 - augmented assignments (``x += 1``)
441 - augmented assignments (``x += 1``)
443 - deletions (``del x``)
442 - deletions (``del x``)
444
443
445 Does not evaluate operations which do not return values:
444 Does not evaluate operations which do not return values:
446
445
447 - assertions (``assert x``)
446 - assertions (``assert x``)
448 - pass (``pass``)
447 - pass (``pass``)
449 - imports (``import x``)
448 - imports (``import x``)
450 - control flow:
449 - control flow:
451
450
452 - conditionals (``if x:``) except for ternary IfExp (``a if x else b``)
451 - conditionals (``if x:``) except for ternary IfExp (``a if x else b``)
453 - loops (``for`` and ``while``)
452 - loops (``for`` and ``while``)
454 - exception handling
453 - exception handling
455
454
456 The purpose of this function is to guard against unwanted side-effects;
455 The purpose of this function is to guard against unwanted side-effects;
457 it does not give guarantees on protection from malicious code execution.
456 it does not give guarantees on protection from malicious code execution.
458 """
457 """
459 policy = EVALUATION_POLICIES[context.evaluation]
458 policy = EVALUATION_POLICIES[context.evaluation]
460 if node is None:
459 if node is None:
461 return None
460 return None
462 if isinstance(node, ast.Expression):
461 if isinstance(node, ast.Expression):
463 return eval_node(node.body, context)
462 return eval_node(node.body, context)
464 if isinstance(node, ast.BinOp):
463 if isinstance(node, ast.BinOp):
465 left = eval_node(node.left, context)
464 left = eval_node(node.left, context)
466 right = eval_node(node.right, context)
465 right = eval_node(node.right, context)
467 dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
466 dunders = _find_dunder(node.op, BINARY_OP_DUNDERS)
468 if dunders:
467 if dunders:
469 if policy.can_operate(dunders, left, right):
468 if policy.can_operate(dunders, left, right):
470 return getattr(left, dunders[0])(right)
469 return getattr(left, dunders[0])(right)
471 else:
470 else:
472 raise GuardRejection(
471 raise GuardRejection(
473 f"Operation (`{dunders}`) for",
472 f"Operation (`{dunders}`) for",
474 type(left),
473 type(left),
475 f"not allowed in {context.evaluation} mode",
474 f"not allowed in {context.evaluation} mode",
476 )
475 )
477 if isinstance(node, ast.Compare):
476 if isinstance(node, ast.Compare):
478 left = eval_node(node.left, context)
477 left = eval_node(node.left, context)
479 all_true = True
478 all_true = True
480 negate = False
479 negate = False
481 for op, right in zip(node.ops, node.comparators):
480 for op, right in zip(node.ops, node.comparators):
482 right = eval_node(right, context)
481 right = eval_node(right, context)
483 dunder = None
482 dunder = None
484 dunders = _find_dunder(op, COMP_OP_DUNDERS)
483 dunders = _find_dunder(op, COMP_OP_DUNDERS)
485 if not dunders:
484 if not dunders:
486 if isinstance(op, ast.NotIn):
485 if isinstance(op, ast.NotIn):
487 dunders = COMP_OP_DUNDERS[ast.In]
486 dunders = COMP_OP_DUNDERS[ast.In]
488 negate = True
487 negate = True
489 if isinstance(op, ast.Is):
488 if isinstance(op, ast.Is):
490 dunder = "is_"
489 dunder = "is_"
491 if isinstance(op, ast.IsNot):
490 if isinstance(op, ast.IsNot):
492 dunder = "is_"
491 dunder = "is_"
493 negate = True
492 negate = True
494 if not dunder and dunders:
493 if not dunder and dunders:
495 dunder = dunders[0]
494 dunder = dunders[0]
496 if dunder:
495 if dunder:
497 a, b = (right, left) if dunder == "__contains__" else (left, right)
496 a, b = (right, left) if dunder == "__contains__" else (left, right)
498 if dunder == "is_" or dunders and policy.can_operate(dunders, a, b):
497 if dunder == "is_" or dunders and policy.can_operate(dunders, a, b):
499 result = getattr(operator, dunder)(a, b)
498 result = getattr(operator, dunder)(a, b)
500 if negate:
499 if negate:
501 result = not result
500 result = not result
502 if not result:
501 if not result:
503 all_true = False
502 all_true = False
504 left = right
503 left = right
505 else:
504 else:
506 raise GuardRejection(
505 raise GuardRejection(
507 f"Comparison (`{dunder}`) for",
506 f"Comparison (`{dunder}`) for",
508 type(left),
507 type(left),
509 f"not allowed in {context.evaluation} mode",
508 f"not allowed in {context.evaluation} mode",
510 )
509 )
511 else:
510 else:
512 raise ValueError(
511 raise ValueError(
513 f"Comparison `{dunder}` not supported"
512 f"Comparison `{dunder}` not supported"
514 ) # pragma: no cover
513 ) # pragma: no cover
515 return all_true
514 return all_true
516 if isinstance(node, ast.Constant):
515 if isinstance(node, ast.Constant):
517 return node.value
516 return node.value
518 if isinstance(node, ast.Tuple):
517 if isinstance(node, ast.Tuple):
519 return tuple(eval_node(e, context) for e in node.elts)
518 return tuple(eval_node(e, context) for e in node.elts)
520 if isinstance(node, ast.List):
519 if isinstance(node, ast.List):
521 return [eval_node(e, context) for e in node.elts]
520 return [eval_node(e, context) for e in node.elts]
522 if isinstance(node, ast.Set):
521 if isinstance(node, ast.Set):
523 return {eval_node(e, context) for e in node.elts}
522 return {eval_node(e, context) for e in node.elts}
524 if isinstance(node, ast.Dict):
523 if isinstance(node, ast.Dict):
525 return dict(
524 return dict(
526 zip(
525 zip(
527 [eval_node(k, context) for k in node.keys],
526 [eval_node(k, context) for k in node.keys],
528 [eval_node(v, context) for v in node.values],
527 [eval_node(v, context) for v in node.values],
529 )
528 )
530 )
529 )
531 if isinstance(node, ast.Slice):
530 if isinstance(node, ast.Slice):
532 return slice(
531 return slice(
533 eval_node(node.lower, context),
532 eval_node(node.lower, context),
534 eval_node(node.upper, context),
533 eval_node(node.upper, context),
535 eval_node(node.step, context),
534 eval_node(node.step, context),
536 )
535 )
537 if isinstance(node, ast.UnaryOp):
536 if isinstance(node, ast.UnaryOp):
538 value = eval_node(node.operand, context)
537 value = eval_node(node.operand, context)
539 dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
538 dunders = _find_dunder(node.op, UNARY_OP_DUNDERS)
540 if dunders:
539 if dunders:
541 if policy.can_operate(dunders, value):
540 if policy.can_operate(dunders, value):
542 return getattr(value, dunders[0])()
541 return getattr(value, dunders[0])()
543 else:
542 else:
544 raise GuardRejection(
543 raise GuardRejection(
545 f"Operation (`{dunders}`) for",
544 f"Operation (`{dunders}`) for",
546 type(value),
545 type(value),
547 f"not allowed in {context.evaluation} mode",
546 f"not allowed in {context.evaluation} mode",
548 )
547 )
549 if isinstance(node, ast.Subscript):
548 if isinstance(node, ast.Subscript):
550 value = eval_node(node.value, context)
549 value = eval_node(node.value, context)
551 slice_ = eval_node(node.slice, context)
550 slice_ = eval_node(node.slice, context)
552 if policy.can_get_item(value, slice_):
551 if policy.can_get_item(value, slice_):
553 return value[slice_]
552 return value[slice_]
554 raise GuardRejection(
553 raise GuardRejection(
555 "Subscript access (`__getitem__`) for",
554 "Subscript access (`__getitem__`) for",
556 type(value), # not joined to avoid calling `repr`
555 type(value), # not joined to avoid calling `repr`
557 f" not allowed in {context.evaluation} mode",
556 f" not allowed in {context.evaluation} mode",
558 )
557 )
559 if isinstance(node, ast.Name):
558 if isinstance(node, ast.Name):
560 if policy.allow_locals_access and node.id in context.locals:
559 if policy.allow_locals_access and node.id in context.locals:
561 return context.locals[node.id]
560 return context.locals[node.id]
562 if policy.allow_globals_access and node.id in context.globals:
561 if policy.allow_globals_access and node.id in context.globals:
563 return context.globals[node.id]
562 return context.globals[node.id]
564 if policy.allow_builtins_access and hasattr(builtins, node.id):
563 if policy.allow_builtins_access and hasattr(builtins, node.id):
565 # note: do not use __builtins__, it is implementation detail of cPython
564 # note: do not use __builtins__, it is implementation detail of cPython
566 return getattr(builtins, node.id)
565 return getattr(builtins, node.id)
567 if not policy.allow_globals_access and not policy.allow_locals_access:
566 if not policy.allow_globals_access and not policy.allow_locals_access:
568 raise GuardRejection(
567 raise GuardRejection(
569 f"Namespace access not allowed in {context.evaluation} mode"
568 f"Namespace access not allowed in {context.evaluation} mode"
570 )
569 )
571 else:
570 else:
572 raise NameError(f"{node.id} not found in locals, globals, nor builtins")
571 raise NameError(f"{node.id} not found in locals, globals, nor builtins")
573 if isinstance(node, ast.Attribute):
572 if isinstance(node, ast.Attribute):
574 value = eval_node(node.value, context)
573 value = eval_node(node.value, context)
575 if policy.can_get_attr(value, node.attr):
574 if policy.can_get_attr(value, node.attr):
576 return getattr(value, node.attr)
575 return getattr(value, node.attr)
577 raise GuardRejection(
576 raise GuardRejection(
578 "Attribute access (`__getattr__`) for",
577 "Attribute access (`__getattr__`) for",
579 type(value), # not joined to avoid calling `repr`
578 type(value), # not joined to avoid calling `repr`
580 f"not allowed in {context.evaluation} mode",
579 f"not allowed in {context.evaluation} mode",
581 )
580 )
582 if isinstance(node, ast.IfExp):
581 if isinstance(node, ast.IfExp):
583 test = eval_node(node.test, context)
582 test = eval_node(node.test, context)
584 if test:
583 if test:
585 return eval_node(node.body, context)
584 return eval_node(node.body, context)
586 else:
585 else:
587 return eval_node(node.orelse, context)
586 return eval_node(node.orelse, context)
588 if isinstance(node, ast.Call):
587 if isinstance(node, ast.Call):
589 func = eval_node(node.func, context)
588 func = eval_node(node.func, context)
590 if policy.can_call(func) and not node.keywords:
589 if policy.can_call(func) and not node.keywords:
591 args = [eval_node(arg, context) for arg in node.args]
590 args = [eval_node(arg, context) for arg in node.args]
592 return func(*args)
591 return func(*args)
593 sig = signature(func)
592 sig = signature(func)
594 # if annotation was not stringized, or it was stringized
593 # if annotation was not stringized, or it was stringized
595 # but resolved by signature call we know the return type
594 # but resolved by signature call we know the return type
596 if not isinstance(sig.return_annotation, str):
595 if not isinstance(sig.return_annotation, str):
597 duck = Duck()
596 duck = Duck()
598 duck.__class__ = sig.return_annotation
597 duck.__class__ = sig.return_annotation
599 return duck
598 return duck
600 raise GuardRejection(
599 raise GuardRejection(
601 "Call for",
600 "Call for",
602 func, # not joined to avoid calling `repr`
601 func, # not joined to avoid calling `repr`
603 f"not allowed in {context.evaluation} mode",
602 f"not allowed in {context.evaluation} mode",
604 )
603 )
605 raise ValueError("Unhandled node", ast.dump(node))
604 raise ValueError("Unhandled node", ast.dump(node))
606
605
607
606
608 SUPPORTED_EXTERNAL_GETITEM = {
607 SUPPORTED_EXTERNAL_GETITEM = {
609 ("pandas", "core", "indexing", "_iLocIndexer"),
608 ("pandas", "core", "indexing", "_iLocIndexer"),
610 ("pandas", "core", "indexing", "_LocIndexer"),
609 ("pandas", "core", "indexing", "_LocIndexer"),
611 ("pandas", "DataFrame"),
610 ("pandas", "DataFrame"),
612 ("pandas", "Series"),
611 ("pandas", "Series"),
613 ("numpy", "ndarray"),
612 ("numpy", "ndarray"),
614 ("numpy", "void"),
613 ("numpy", "void"),
615 }
614 }
616
615
617
616
618 BUILTIN_GETITEM: Set[InstancesHaveGetItem] = {
617 BUILTIN_GETITEM: Set[InstancesHaveGetItem] = {
619 dict,
618 dict,
620 str, # type: ignore[arg-type]
619 str, # type: ignore[arg-type]
621 bytes, # type: ignore[arg-type]
620 bytes, # type: ignore[arg-type]
622 list,
621 list,
623 tuple,
622 tuple,
624 collections.defaultdict,
623 collections.defaultdict,
625 collections.deque,
624 collections.deque,
626 collections.OrderedDict,
625 collections.OrderedDict,
627 collections.ChainMap,
626 collections.ChainMap,
628 collections.UserDict,
627 collections.UserDict,
629 collections.UserList,
628 collections.UserList,
630 collections.UserString, # type: ignore[arg-type]
629 collections.UserString, # type: ignore[arg-type]
631 _DummyNamedTuple,
630 _DummyNamedTuple,
632 _IdentitySubscript,
631 _IdentitySubscript,
633 }
632 }
634
633
635
634
636 def _list_methods(cls, source=None):
635 def _list_methods(cls, source=None):
637 """For use on immutable objects or with methods returning a copy"""
636 """For use on immutable objects or with methods returning a copy"""
638 return [getattr(cls, k) for k in (source if source else dir(cls))]
637 return [getattr(cls, k) for k in (source if source else dir(cls))]
639
638
640
639
641 dict_non_mutating_methods = ("copy", "keys", "values", "items")
640 dict_non_mutating_methods = ("copy", "keys", "values", "items")
642 list_non_mutating_methods = ("copy", "index", "count")
641 list_non_mutating_methods = ("copy", "index", "count")
643 set_non_mutating_methods = set(dir(set)) & set(dir(frozenset))
642 set_non_mutating_methods = set(dir(set)) & set(dir(frozenset))
644
643
645
644
646 dict_keys: Type[collections.abc.KeysView] = type({}.keys())
645 dict_keys: Type[collections.abc.KeysView] = type({}.keys())
647
646
648 NUMERICS = {int, float, complex}
647 NUMERICS = {int, float, complex}
649
648
650 ALLOWED_CALLS = {
649 ALLOWED_CALLS = {
651 bytes,
650 bytes,
652 *_list_methods(bytes),
651 *_list_methods(bytes),
653 dict,
652 dict,
654 *_list_methods(dict, dict_non_mutating_methods),
653 *_list_methods(dict, dict_non_mutating_methods),
655 dict_keys.isdisjoint,
654 dict_keys.isdisjoint,
656 list,
655 list,
657 *_list_methods(list, list_non_mutating_methods),
656 *_list_methods(list, list_non_mutating_methods),
658 set,
657 set,
659 *_list_methods(set, set_non_mutating_methods),
658 *_list_methods(set, set_non_mutating_methods),
660 frozenset,
659 frozenset,
661 *_list_methods(frozenset),
660 *_list_methods(frozenset),
662 range,
661 range,
663 str,
662 str,
664 *_list_methods(str),
663 *_list_methods(str),
665 tuple,
664 tuple,
666 *_list_methods(tuple),
665 *_list_methods(tuple),
667 *NUMERICS,
666 *NUMERICS,
668 *[method for numeric_cls in NUMERICS for method in _list_methods(numeric_cls)],
667 *[method for numeric_cls in NUMERICS for method in _list_methods(numeric_cls)],
669 collections.deque,
668 collections.deque,
670 *_list_methods(collections.deque, list_non_mutating_methods),
669 *_list_methods(collections.deque, list_non_mutating_methods),
671 collections.defaultdict,
670 collections.defaultdict,
672 *_list_methods(collections.defaultdict, dict_non_mutating_methods),
671 *_list_methods(collections.defaultdict, dict_non_mutating_methods),
673 collections.OrderedDict,
672 collections.OrderedDict,
674 *_list_methods(collections.OrderedDict, dict_non_mutating_methods),
673 *_list_methods(collections.OrderedDict, dict_non_mutating_methods),
675 collections.UserDict,
674 collections.UserDict,
676 *_list_methods(collections.UserDict, dict_non_mutating_methods),
675 *_list_methods(collections.UserDict, dict_non_mutating_methods),
677 collections.UserList,
676 collections.UserList,
678 *_list_methods(collections.UserList, list_non_mutating_methods),
677 *_list_methods(collections.UserList, list_non_mutating_methods),
679 collections.UserString,
678 collections.UserString,
680 *_list_methods(collections.UserString, dir(str)),
679 *_list_methods(collections.UserString, dir(str)),
681 collections.Counter,
680 collections.Counter,
682 *_list_methods(collections.Counter, dict_non_mutating_methods),
681 *_list_methods(collections.Counter, dict_non_mutating_methods),
683 collections.Counter.elements,
682 collections.Counter.elements,
684 collections.Counter.most_common,
683 collections.Counter.most_common,
685 }
684 }
686
685
687 BUILTIN_GETATTR: Set[MayHaveGetattr] = {
686 BUILTIN_GETATTR: Set[MayHaveGetattr] = {
688 *BUILTIN_GETITEM,
687 *BUILTIN_GETITEM,
689 set,
688 set,
690 frozenset,
689 frozenset,
691 object,
690 object,
692 type, # `type` handles a lot of generic cases, e.g. numbers as in `int.real`.
691 type, # `type` handles a lot of generic cases, e.g. numbers as in `int.real`.
693 *NUMERICS,
692 *NUMERICS,
694 dict_keys,
693 dict_keys,
695 MethodDescriptorType,
694 MethodDescriptorType,
696 ModuleType,
695 ModuleType,
697 }
696 }
698
697
699
698
700 BUILTIN_OPERATIONS = {*BUILTIN_GETATTR}
699 BUILTIN_OPERATIONS = {*BUILTIN_GETATTR}
701
700
702 EVALUATION_POLICIES = {
701 EVALUATION_POLICIES = {
703 "minimal": EvaluationPolicy(
702 "minimal": EvaluationPolicy(
704 allow_builtins_access=True,
703 allow_builtins_access=True,
705 allow_locals_access=False,
704 allow_locals_access=False,
706 allow_globals_access=False,
705 allow_globals_access=False,
707 allow_item_access=False,
706 allow_item_access=False,
708 allow_attr_access=False,
707 allow_attr_access=False,
709 allowed_calls=set(),
708 allowed_calls=set(),
710 allow_any_calls=False,
709 allow_any_calls=False,
711 allow_all_operations=False,
710 allow_all_operations=False,
712 ),
711 ),
713 "limited": SelectivePolicy(
712 "limited": SelectivePolicy(
714 allowed_getitem=BUILTIN_GETITEM,
713 allowed_getitem=BUILTIN_GETITEM,
715 allowed_getitem_external=SUPPORTED_EXTERNAL_GETITEM,
714 allowed_getitem_external=SUPPORTED_EXTERNAL_GETITEM,
716 allowed_getattr=BUILTIN_GETATTR,
715 allowed_getattr=BUILTIN_GETATTR,
717 allowed_getattr_external={
716 allowed_getattr_external={
718 # pandas Series/Frame implements custom `__getattr__`
717 # pandas Series/Frame implements custom `__getattr__`
719 ("pandas", "DataFrame"),
718 ("pandas", "DataFrame"),
720 ("pandas", "Series"),
719 ("pandas", "Series"),
721 },
720 },
722 allowed_operations=BUILTIN_OPERATIONS,
721 allowed_operations=BUILTIN_OPERATIONS,
723 allow_builtins_access=True,
722 allow_builtins_access=True,
724 allow_locals_access=True,
723 allow_locals_access=True,
725 allow_globals_access=True,
724 allow_globals_access=True,
726 allowed_calls=ALLOWED_CALLS,
725 allowed_calls=ALLOWED_CALLS,
727 ),
726 ),
728 "unsafe": EvaluationPolicy(
727 "unsafe": EvaluationPolicy(
729 allow_builtins_access=True,
728 allow_builtins_access=True,
730 allow_locals_access=True,
729 allow_locals_access=True,
731 allow_globals_access=True,
730 allow_globals_access=True,
732 allow_attr_access=True,
731 allow_attr_access=True,
733 allow_item_access=True,
732 allow_item_access=True,
734 allow_any_calls=True,
733 allow_any_calls=True,
735 allow_all_operations=True,
734 allow_all_operations=True,
736 ),
735 ),
737 }
736 }
738
737
739
738
740 __all__ = [
739 __all__ = [
741 "guarded_eval",
740 "guarded_eval",
742 "eval_node",
741 "eval_node",
743 "GuardRejection",
742 "GuardRejection",
744 "EvaluationContext",
743 "EvaluationContext",
745 "_unbind_method",
744 "_unbind_method",
746 ]
745 ]
General Comments 0
You need to be logged in to leave comments. Login now