Show More
@@ -996,7 +996,8 b' class Completer(Configurable):' | |||||
996 |
|
996 | |||
997 | - ``forbidden``: no evaluation of code is permitted, |
|
997 | - ``forbidden``: no evaluation of code is permitted, | |
998 | - ``minimal``: evaluation of literals and access to built-in namespace; |
|
998 | - ``minimal``: evaluation of literals and access to built-in namespace; | |
999 |
no item/attribute evaluation no |
|
999 | no item/attribute evaluationm no access to locals/globals, | |
|
1000 | no evaluation of any operations or comparisons. | |||
1000 | - ``limited``: access to all namespaces, evaluation of hard-coded methods |
|
1001 | - ``limited``: access to all namespaces, evaluation of hard-coded methods | |
1001 | (for example: :any:`dict.keys`, :any:`object.__getattr__`, |
|
1002 | (for example: :any:`dict.keys`, :any:`object.__getattr__`, | |
1002 | :any:`object.__getitem__`) on allow-listed objects (for example: |
|
1003 | :any:`object.__getitem__`) on allow-listed objects (for example: |
@@ -108,6 +108,7 b' class EvaluationPolicy:' | |||||
108 | return True |
|
108 | return True | |
109 |
|
109 | |||
110 | owner_method = _unbind_method(func) |
|
110 | owner_method = _unbind_method(func) | |
|
111 | ||||
111 | if owner_method and owner_method in self.allowed_calls: |
|
112 | if owner_method and owner_method in self.allowed_calls: | |
112 | return True |
|
113 | return True | |
113 |
|
114 | |||
@@ -127,6 +128,10 b' def _has_original_dunder_external(' | |||||
127 | value_type = type(value) |
|
128 | value_type = type(value) | |
128 | if type(value) == member_type: |
|
129 | if type(value) == member_type: | |
129 | return True |
|
130 | return True | |
|
131 | if method_name == "__getattribute__": | |||
|
132 | # we have to short-circuit here due to an unresolved issue in | |||
|
133 | # `isinstance` implementation: https://bugs.python.org/issue32683 | |||
|
134 | return False | |||
130 | if isinstance(value, member_type): |
|
135 | if isinstance(value, member_type): | |
131 | method = getattr(value_type, method_name, None) |
|
136 | method = getattr(value_type, method_name, None) | |
132 | member_method = getattr(member_type, method_name, None) |
|
137 | member_method = getattr(member_type, method_name, None) | |
@@ -149,7 +154,7 b' def _has_original_dunder(' | |||||
149 |
|
154 | |||
150 | method = getattr(value_type, method_name, None) |
|
155 | method = getattr(value_type, method_name, None) | |
151 |
|
156 | |||
152 |
if |
|
157 | if method is None: | |
153 | return None |
|
158 | return None | |
154 |
|
159 | |||
155 | if method in allowed_methods: |
|
160 | if method in allowed_methods: | |
@@ -193,6 +198,7 b' class SelectivePolicy(EvaluationPolicy):' | |||||
193 | allowed_external=self.allowed_getattr_external, |
|
198 | allowed_external=self.allowed_getattr_external, | |
194 | method_name="__getattr__", |
|
199 | method_name="__getattr__", | |
195 | ) |
|
200 | ) | |
|
201 | ||||
196 | # Many objects do not have `__getattr__`, this is fine |
|
202 | # Many objects do not have `__getattr__`, this is fine | |
197 | if has_original_attr is None and has_original_attribute: |
|
203 | if has_original_attr is None and has_original_attribute: | |
198 | return True |
|
204 | return True | |
@@ -200,10 +206,6 b' class SelectivePolicy(EvaluationPolicy):' | |||||
200 | # Accept objects without modifications to `__getattr__` and `__getattribute__` |
|
206 | # Accept objects without modifications to `__getattr__` and `__getattribute__` | |
201 | return has_original_attr and has_original_attribute |
|
207 | return has_original_attr and has_original_attribute | |
202 |
|
208 | |||
203 | def get_attr(self, value, attr): |
|
|||
204 | if self.can_get_attr(value, attr): |
|
|||
205 | return getattr(value, attr) |
|
|||
206 |
|
||||
207 | def can_get_item(self, value, item): |
|
209 | def can_get_item(self, value, item): | |
208 | """Allow accessing `__getiitem__` of allow-listed instances unless it was not modified.""" |
|
210 | """Allow accessing `__getiitem__` of allow-listed instances unless it was not modified.""" | |
209 | return _has_original_dunder( |
|
211 | return _has_original_dunder( | |
@@ -215,20 +217,24 b' class SelectivePolicy(EvaluationPolicy):' | |||||
215 | ) |
|
217 | ) | |
216 |
|
218 | |||
217 | def can_operate(self, dunders: Tuple[str, ...], a, b=None): |
|
219 | def can_operate(self, dunders: Tuple[str, ...], a, b=None): | |
|
220 | objects = [a] | |||
|
221 | if b is not None: | |||
|
222 | objects.append(b) | |||
218 | return all( |
|
223 | return all( | |
219 | [ |
|
224 | [ | |
220 | _has_original_dunder( |
|
225 | _has_original_dunder( | |
221 |
|
|
226 | obj, | |
222 | allowed_types=self.allowed_operations, |
|
227 | allowed_types=self.allowed_operations, | |
223 | allowed_methods=self._dunder_methods(dunder), |
|
228 | allowed_methods=self._operator_dunder_methods(dunder), | |
224 | allowed_external=self.allowed_operations_external, |
|
229 | allowed_external=self.allowed_operations_external, | |
225 | method_name=dunder, |
|
230 | method_name=dunder, | |
226 | ) |
|
231 | ) | |
227 | for dunder in dunders |
|
232 | for dunder in dunders | |
|
233 | for obj in objects | |||
228 | ] |
|
234 | ] | |
229 | ) |
|
235 | ) | |
230 |
|
236 | |||
231 | def _dunder_methods(self, dunder: str) -> Set[Callable]: |
|
237 | def _operator_dunder_methods(self, dunder: str) -> Set[Callable]: | |
232 | if dunder not in self._operation_methods_cache: |
|
238 | if dunder not in self._operation_methods_cache: | |
233 | self._operation_methods_cache[dunder] = self._safe_get_methods( |
|
239 | self._operation_methods_cache[dunder] = self._safe_get_methods( | |
234 | self.allowed_operations, dunder |
|
240 | self.allowed_operations, dunder | |
@@ -257,7 +263,7 b' class SelectivePolicy(EvaluationPolicy):' | |||||
257 |
|
263 | |||
258 |
|
264 | |||
259 | class _DummyNamedTuple(NamedTuple): |
|
265 | class _DummyNamedTuple(NamedTuple): | |
260 | pass |
|
266 | """Used internally to retrieve methods of named tuple instance.""" | |
261 |
|
267 | |||
262 |
|
268 | |||
263 | class EvaluationContext(NamedTuple): |
|
269 | class EvaluationContext(NamedTuple): | |
@@ -451,12 +457,15 b' def eval_node(node: Union[ast.AST, None], context: EvaluationContext):' | |||||
451 | f"not allowed in {context.evaluation} mode", |
|
457 | f"not allowed in {context.evaluation} mode", | |
452 | ) |
|
458 | ) | |
453 | else: |
|
459 | else: | |
454 |
raise ValueError( |
|
460 | raise ValueError( | |
|
461 | f"Comparison `{dunder}` not supported" | |||
|
462 | ) # pragma: no cover | |||
455 | return all_true |
|
463 | return all_true | |
456 | if isinstance(node, ast.Constant): |
|
464 | if isinstance(node, ast.Constant): | |
457 | return node.value |
|
465 | return node.value | |
458 | if isinstance(node, ast.Index): |
|
466 | if isinstance(node, ast.Index): | |
459 | return eval_node(node.value, context) |
|
467 | # deprecated since Python 3.9 | |
|
468 | return eval_node(node.value, context) # pragma: no cover | |||
460 | if isinstance(node, ast.Tuple): |
|
469 | if isinstance(node, ast.Tuple): | |
461 | return tuple(eval_node(e, context) for e in node.elts) |
|
470 | return tuple(eval_node(e, context) for e in node.elts) | |
462 | if isinstance(node, ast.List): |
|
471 | if isinstance(node, ast.List): | |
@@ -477,7 +486,8 b' def eval_node(node: Union[ast.AST, None], context: EvaluationContext):' | |||||
477 | eval_node(node.step, context), |
|
486 | eval_node(node.step, context), | |
478 | ) |
|
487 | ) | |
479 | if isinstance(node, ast.ExtSlice): |
|
488 | if isinstance(node, ast.ExtSlice): | |
480 | return tuple([eval_node(dim, context) for dim in node.dims]) |
|
489 | # deprecated since Python 3.9 | |
|
490 | return tuple([eval_node(dim, context) for dim in node.dims]) # pragma: no cover | |||
481 | if isinstance(node, ast.UnaryOp): |
|
491 | if isinstance(node, ast.UnaryOp): | |
482 | value = eval_node(node.operand, context) |
|
492 | value = eval_node(node.operand, context) | |
483 | dunders = _find_dunder(node.op, UNARY_OP_DUNDERS) |
|
493 | dunders = _find_dunder(node.op, UNARY_OP_DUNDERS) | |
@@ -490,7 +500,6 b' def eval_node(node: Union[ast.AST, None], context: EvaluationContext):' | |||||
490 | type(value), |
|
500 | type(value), | |
491 | f"not allowed in {context.evaluation} mode", |
|
501 | f"not allowed in {context.evaluation} mode", | |
492 | ) |
|
502 | ) | |
493 | raise ValueError("Unhandled unary operation:", node.op) |
|
|||
494 | if isinstance(node, ast.Subscript): |
|
503 | if isinstance(node, ast.Subscript): | |
495 | value = eval_node(node.value, context) |
|
504 | value = eval_node(node.value, context) | |
496 | slice_ = eval_node(node.slice, context) |
|
505 | slice_ = eval_node(node.slice, context) | |
@@ -507,14 +516,14 b' def eval_node(node: Union[ast.AST, None], context: EvaluationContext):' | |||||
507 | if policy.allow_globals_access and node.id in context.globals: |
|
516 | if policy.allow_globals_access and node.id in context.globals: | |
508 | return context.globals[node.id] |
|
517 | return context.globals[node.id] | |
509 | if policy.allow_builtins_access and hasattr(builtins, node.id): |
|
518 | if policy.allow_builtins_access and hasattr(builtins, node.id): | |
510 | # note: do not use __builtins__, it is implementation detail of Python |
|
519 | # note: do not use __builtins__, it is implementation detail of cPython | |
511 | return getattr(builtins, node.id) |
|
520 | return getattr(builtins, node.id) | |
512 | if not policy.allow_globals_access and not policy.allow_locals_access: |
|
521 | if not policy.allow_globals_access and not policy.allow_locals_access: | |
513 | raise GuardRejection( |
|
522 | raise GuardRejection( | |
514 | f"Namespace access not allowed in {context.evaluation} mode" |
|
523 | f"Namespace access not allowed in {context.evaluation} mode" | |
515 | ) |
|
524 | ) | |
516 | else: |
|
525 | else: | |
517 |
raise NameError(f"{node.id} not found in locals |
|
526 | raise NameError(f"{node.id} not found in locals, globals, nor builtins") | |
518 | if isinstance(node, ast.Attribute): |
|
527 | if isinstance(node, ast.Attribute): | |
519 | value = eval_node(node.value, context) |
|
528 | value = eval_node(node.value, context) | |
520 | if policy.can_get_attr(value, node.attr): |
|
529 | if policy.can_get_attr(value, node.attr): | |
@@ -540,7 +549,7 b' def eval_node(node: Union[ast.AST, None], context: EvaluationContext):' | |||||
540 | func, # not joined to avoid calling `repr` |
|
549 | func, # not joined to avoid calling `repr` | |
541 | f"not allowed in {context.evaluation} mode", |
|
550 | f"not allowed in {context.evaluation} mode", | |
542 | ) |
|
551 | ) | |
543 | raise ValueError("Unhandled node", node) |
|
552 | raise ValueError("Unhandled node", ast.dump(node)) | |
544 |
|
553 | |||
545 |
|
554 | |||
546 | SUPPORTED_EXTERNAL_GETITEM = { |
|
555 | SUPPORTED_EXTERNAL_GETITEM = { | |
@@ -552,6 +561,7 b' SUPPORTED_EXTERNAL_GETITEM = {' | |||||
552 | ("numpy", "void"), |
|
561 | ("numpy", "void"), | |
553 | } |
|
562 | } | |
554 |
|
563 | |||
|
564 | ||||
555 | BUILTIN_GETITEM: Set[InstancesHaveGetItem] = { |
|
565 | BUILTIN_GETITEM: Set[InstancesHaveGetItem] = { | |
556 | dict, |
|
566 | dict, | |
557 | str, |
|
567 | str, | |
@@ -583,6 +593,8 b' set_non_mutating_methods = set(dir(set)) & set(dir(frozenset))' | |||||
583 | dict_keys: Type[collections.abc.KeysView] = type({}.keys()) |
|
593 | dict_keys: Type[collections.abc.KeysView] = type({}.keys()) | |
584 | method_descriptor: Any = type(list.copy) |
|
594 | method_descriptor: Any = type(list.copy) | |
585 |
|
595 | |||
|
596 | NUMERICS = {int, float, complex} | |||
|
597 | ||||
586 | ALLOWED_CALLS = { |
|
598 | ALLOWED_CALLS = { | |
587 | bytes, |
|
599 | bytes, | |
588 | *_list_methods(bytes), |
|
600 | *_list_methods(bytes), | |
@@ -600,6 +612,8 b' ALLOWED_CALLS = {' | |||||
600 | *_list_methods(str), |
|
612 | *_list_methods(str), | |
601 | tuple, |
|
613 | tuple, | |
602 | *_list_methods(tuple), |
|
614 | *_list_methods(tuple), | |
|
615 | *NUMERICS, | |||
|
616 | *[method for numeric_cls in NUMERICS for method in _list_methods(numeric_cls)], | |||
603 | collections.deque, |
|
617 | collections.deque, | |
604 | *_list_methods(collections.deque, list_non_mutating_methods), |
|
618 | *_list_methods(collections.deque, list_non_mutating_methods), | |
605 | collections.defaultdict, |
|
619 | collections.defaultdict, | |
@@ -624,12 +638,13 b' BUILTIN_GETATTR: Set[MayHaveGetattr] = {' | |||||
624 | frozenset, |
|
638 | frozenset, | |
625 | object, |
|
639 | object, | |
626 | type, # `type` handles a lot of generic cases, e.g. numbers as in `int.real`. |
|
640 | type, # `type` handles a lot of generic cases, e.g. numbers as in `int.real`. | |
|
641 | *NUMERICS, | |||
627 | dict_keys, |
|
642 | dict_keys, | |
628 | method_descriptor, |
|
643 | method_descriptor, | |
629 | } |
|
644 | } | |
630 |
|
645 | |||
631 |
|
646 | |||
632 |
BUILTIN_OPERATIONS = { |
|
647 | BUILTIN_OPERATIONS = {*BUILTIN_GETATTR} | |
633 |
|
648 | |||
634 | EVALUATION_POLICIES = { |
|
649 | EVALUATION_POLICIES = { | |
635 | "minimal": EvaluationPolicy( |
|
650 | "minimal": EvaluationPolicy( |
@@ -1,4 +1,5 b'' | |||||
1 | from typing import NamedTuple |
|
1 | from typing import NamedTuple | |
|
2 | from functools import partial | |||
2 | from IPython.core.guarded_eval import ( |
|
3 | from IPython.core.guarded_eval import ( | |
3 | EvaluationContext, |
|
4 | EvaluationContext, | |
4 | GuardRejection, |
|
5 | GuardRejection, | |
@@ -9,12 +10,19 b' from IPython.testing import decorators as dec' | |||||
9 | import pytest |
|
10 | import pytest | |
10 |
|
11 | |||
11 |
|
12 | |||
12 | def limited(**kwargs): |
|
13 | def create_context(evaluation: str, **kwargs): | |
13 |
return EvaluationContext(locals=kwargs, globals={}, evaluation= |
|
14 | return EvaluationContext(locals=kwargs, globals={}, evaluation=evaluation) | |
14 |
|
15 | |||
15 |
|
16 | |||
16 | def unsafe(**kwargs): |
|
17 | forbidden = partial(create_context, "forbidden") | |
17 | return EvaluationContext(locals=kwargs, globals={}, evaluation="unsafe") |
|
18 | minimal = partial(create_context, "minimal") | |
|
19 | limited = partial(create_context, "limited") | |||
|
20 | unsafe = partial(create_context, "unsafe") | |||
|
21 | dangerous = partial(create_context, "dangerous") | |||
|
22 | ||||
|
23 | LIMITED_OR_HIGHER = [limited, unsafe, dangerous] | |||
|
24 | ||||
|
25 | MINIMAL_OR_HIGHER = [minimal, *LIMITED_OR_HIGHER] | |||
18 |
|
26 | |||
19 |
|
27 | |||
20 | @dec.skip_without("pandas") |
|
28 | @dec.skip_without("pandas") | |
@@ -142,7 +150,7 b' def test_set_literal():' | |||||
142 | assert guarded_eval('{"a"}', context) == {"a"} |
|
150 | assert guarded_eval('{"a"}', context) == {"a"} | |
143 |
|
151 | |||
144 |
|
152 | |||
145 | def test_if_expression(): |
|
153 | def test_evaluates_if_expression(): | |
146 | context = limited() |
|
154 | context = limited() | |
147 | assert guarded_eval("2 if True else 3", context) == 2 |
|
155 | assert guarded_eval("2 if True else 3", context) == 2 | |
148 | assert guarded_eval("4 if False else 5", context) == 5 |
|
156 | assert guarded_eval("4 if False else 5", context) == 5 | |
@@ -178,7 +186,7 b' def test_method_descriptor():' | |||||
178 | [{"a": 1}, "data.keys().isdisjoint({})", "data.update()", True], |
|
186 | [{"a": 1}, "data.keys().isdisjoint({})", "data.update()", True], | |
179 | ], |
|
187 | ], | |
180 | ) |
|
188 | ) | |
181 | def test_calls(data, good, bad, expected): |
|
189 | def test_evaluates_calls(data, good, bad, expected): | |
182 | context = limited(data=data) |
|
190 | context = limited(data=data) | |
183 | assert guarded_eval(good, context) == expected |
|
191 | assert guarded_eval(good, context) == expected | |
184 |
|
192 | |||
@@ -194,9 +202,26 b' def test_calls(data, good, bad, expected):' | |||||
194 | ["list(range(20))[3:-2:3]", [3, 6, 9, 12, 15]], |
|
202 | ["list(range(20))[3:-2:3]", [3, 6, 9, 12, 15]], | |
195 | ], |
|
203 | ], | |
196 | ) |
|
204 | ) | |
197 | def test_literals(code, expected): |
|
205 | @pytest.mark.parametrize("context", LIMITED_OR_HIGHER) | |
198 | context = limited() |
|
206 | def test_evaluates_complex_cases(code, expected, context): | |
199 | assert guarded_eval(code, context) == expected |
|
207 | assert guarded_eval(code, context()) == expected | |
|
208 | ||||
|
209 | ||||
|
210 | @pytest.mark.parametrize( | |||
|
211 | "code,expected", | |||
|
212 | [ | |||
|
213 | ["1", 1], | |||
|
214 | ["1.0", 1.0], | |||
|
215 | ["0xdeedbeef", 0xDEEDBEEF], | |||
|
216 | ["True", True], | |||
|
217 | ["None", None], | |||
|
218 | ["{}", {}], | |||
|
219 | ["[]", []], | |||
|
220 | ], | |||
|
221 | ) | |||
|
222 | @pytest.mark.parametrize("context", MINIMAL_OR_HIGHER) | |||
|
223 | def test_evaluates_literals(code, expected, context): | |||
|
224 | assert guarded_eval(code, context()) == expected | |||
200 |
|
225 | |||
201 |
|
226 | |||
202 | @pytest.mark.parametrize( |
|
227 | @pytest.mark.parametrize( | |
@@ -207,9 +232,9 b' def test_literals(code, expected):' | |||||
207 | ["~5", -6], |
|
232 | ["~5", -6], | |
208 | ], |
|
233 | ], | |
209 | ) |
|
234 | ) | |
210 | def test_unary_operations(code, expected): |
|
235 | @pytest.mark.parametrize("context", LIMITED_OR_HIGHER) | |
211 | context = limited() |
|
236 | def test_evaluates_unary_operations(code, expected, context): | |
212 | assert guarded_eval(code, context) == expected |
|
237 | assert guarded_eval(code, context()) == expected | |
213 |
|
238 | |||
214 |
|
239 | |||
215 | @pytest.mark.parametrize( |
|
240 | @pytest.mark.parametrize( | |
@@ -228,9 +253,9 b' def test_unary_operations(code, expected):' | |||||
228 | ["1 & 2", 0], |
|
253 | ["1 & 2", 0], | |
229 | ], |
|
254 | ], | |
230 | ) |
|
255 | ) | |
231 | def test_binary_operations(code, expected): |
|
256 | @pytest.mark.parametrize("context", LIMITED_OR_HIGHER) | |
232 | context = limited() |
|
257 | def test_evaluates_binary_operations(code, expected, context): | |
233 | assert guarded_eval(code, context) == expected |
|
258 | assert guarded_eval(code, context()) == expected | |
234 |
|
259 | |||
235 |
|
260 | |||
236 | @pytest.mark.parametrize( |
|
261 | @pytest.mark.parametrize( | |
@@ -262,16 +287,152 b' def test_binary_operations(code, expected):' | |||||
262 | ["True is True", True], |
|
287 | ["True is True", True], | |
263 | ["False is False", True], |
|
288 | ["False is False", True], | |
264 | ["True is False", False], |
|
289 | ["True is False", False], | |
|
290 | ["True is not True", False], | |||
|
291 | ["False is not True", True], | |||
265 | ], |
|
292 | ], | |
266 | ) |
|
293 | ) | |
267 | def test_comparisons(code, expected): |
|
294 | @pytest.mark.parametrize("context", LIMITED_OR_HIGHER) | |
268 | context = limited() |
|
295 | def test_evaluates_comparisons(code, expected, context): | |
269 | assert guarded_eval(code, context) == expected |
|
296 | assert guarded_eval(code, context()) == expected | |
|
297 | ||||
|
298 | ||||
|
299 | def test_guards_comparisons(): | |||
|
300 | class GoodEq(int): | |||
|
301 | pass | |||
|
302 | ||||
|
303 | class BadEq(int): | |||
|
304 | def __eq__(self, other): | |||
|
305 | assert False | |||
|
306 | ||||
|
307 | context = limited(bad=BadEq(1), good=GoodEq(1)) | |||
|
308 | ||||
|
309 | with pytest.raises(GuardRejection): | |||
|
310 | guarded_eval("bad == 1", context) | |||
|
311 | ||||
|
312 | with pytest.raises(GuardRejection): | |||
|
313 | guarded_eval("bad != 1", context) | |||
|
314 | ||||
|
315 | with pytest.raises(GuardRejection): | |||
|
316 | guarded_eval("1 == bad", context) | |||
|
317 | ||||
|
318 | with pytest.raises(GuardRejection): | |||
|
319 | guarded_eval("1 != bad", context) | |||
|
320 | ||||
|
321 | assert guarded_eval("good == 1", context) is True | |||
|
322 | assert guarded_eval("good != 1", context) is False | |||
|
323 | assert guarded_eval("1 == good", context) is True | |||
|
324 | assert guarded_eval("1 != good", context) is False | |||
|
325 | ||||
|
326 | ||||
|
327 | def test_guards_unary_operations(): | |||
|
328 | class GoodOp(int): | |||
|
329 | pass | |||
|
330 | ||||
|
331 | class BadOpInv(int): | |||
|
332 | def __inv__(self, other): | |||
|
333 | assert False | |||
|
334 | ||||
|
335 | class BadOpInverse(int): | |||
|
336 | def __inv__(self, other): | |||
|
337 | assert False | |||
|
338 | ||||
|
339 | context = limited(good=GoodOp(1), bad1=BadOpInv(1), bad2=BadOpInverse(1)) | |||
|
340 | ||||
|
341 | with pytest.raises(GuardRejection): | |||
|
342 | guarded_eval("~bad1", context) | |||
|
343 | ||||
|
344 | with pytest.raises(GuardRejection): | |||
|
345 | guarded_eval("~bad2", context) | |||
|
346 | ||||
|
347 | ||||
|
348 | def test_guards_binary_operations(): | |||
|
349 | class GoodOp(int): | |||
|
350 | pass | |||
270 |
|
351 | |||
|
352 | class BadOp(int): | |||
|
353 | def __add__(self, other): | |||
|
354 | assert False | |||
271 |
|
355 | |||
272 | def test_access_builtins(): |
|
356 | context = limited(good=GoodOp(1), bad=BadOp(1)) | |
|
357 | ||||
|
358 | with pytest.raises(GuardRejection): | |||
|
359 | guarded_eval("1 + bad", context) | |||
|
360 | ||||
|
361 | with pytest.raises(GuardRejection): | |||
|
362 | guarded_eval("bad + 1", context) | |||
|
363 | ||||
|
364 | assert guarded_eval("good + 1", context) == 2 | |||
|
365 | assert guarded_eval("1 + good", context) == 2 | |||
|
366 | ||||
|
367 | ||||
|
368 | def test_guards_attributes(): | |||
|
369 | class GoodAttr(float): | |||
|
370 | pass | |||
|
371 | ||||
|
372 | class BadAttr1(float): | |||
|
373 | def __getattr__(self, key): | |||
|
374 | assert False | |||
|
375 | ||||
|
376 | class BadAttr2(float): | |||
|
377 | def __getattribute__(self, key): | |||
|
378 | assert False | |||
|
379 | ||||
|
380 | context = limited(good=GoodAttr(0.5), bad1=BadAttr1(0.5), bad2=BadAttr2(0.5)) | |||
|
381 | ||||
|
382 | with pytest.raises(GuardRejection): | |||
|
383 | guarded_eval("bad1.as_integer_ratio", context) | |||
|
384 | ||||
|
385 | with pytest.raises(GuardRejection): | |||
|
386 | guarded_eval("bad2.as_integer_ratio", context) | |||
|
387 | ||||
|
388 | assert guarded_eval("good.as_integer_ratio()", context) == (1, 2) | |||
|
389 | ||||
|
390 | ||||
|
391 | @pytest.mark.parametrize("context", MINIMAL_OR_HIGHER) | |||
|
392 | def test_access_builtins(context): | |||
|
393 | assert guarded_eval("round", context()) == round | |||
|
394 | ||||
|
395 | ||||
|
396 | def test_access_builtins_fails(): | |||
273 | context = limited() |
|
397 | context = limited() | |
274 | assert guarded_eval("round", context) == round |
|
398 | with pytest.raises(NameError): | |
|
399 | guarded_eval("this_is_not_builtin", context) | |||
|
400 | ||||
|
401 | ||||
|
402 | def test_rejects_forbidden(): | |||
|
403 | context = forbidden() | |||
|
404 | with pytest.raises(GuardRejection): | |||
|
405 | guarded_eval("1", context) | |||
|
406 | ||||
|
407 | ||||
|
408 | def test_guards_locals_and_globals(): | |||
|
409 | context = EvaluationContext( | |||
|
410 | locals={"local_a": "a"}, globals={"global_b": "b"}, evaluation="minimal" | |||
|
411 | ) | |||
|
412 | ||||
|
413 | with pytest.raises(GuardRejection): | |||
|
414 | guarded_eval("local_a", context) | |||
|
415 | ||||
|
416 | with pytest.raises(GuardRejection): | |||
|
417 | guarded_eval("global_b", context) | |||
|
418 | ||||
|
419 | ||||
|
420 | def test_access_locals_and_globals(): | |||
|
421 | context = EvaluationContext( | |||
|
422 | locals={"local_a": "a"}, globals={"global_b": "b"}, evaluation="limited" | |||
|
423 | ) | |||
|
424 | assert guarded_eval("local_a", context) == "a" | |||
|
425 | assert guarded_eval("global_b", context) == "b" | |||
|
426 | ||||
|
427 | ||||
|
428 | @pytest.mark.parametrize( | |||
|
429 | "code", | |||
|
430 | ["def func(): pass", "class C: pass", "x = 1", "x += 1", "del x", "import ast"], | |||
|
431 | ) | |||
|
432 | @pytest.mark.parametrize("context", [minimal(), limited(), unsafe()]) | |||
|
433 | def test_rejects_side_effect_syntax(code, context): | |||
|
434 | with pytest.raises(SyntaxError): | |||
|
435 | guarded_eval(code, context) | |||
275 |
|
436 | |||
276 |
|
437 | |||
277 | def test_subscript(): |
|
438 | def test_subscript(): |
General Comments 0
You need to be logged in to leave comments.
Login now