From 92f32aec80bf1382746eaf7d3203c0619a2b41ab Mon Sep 17 00:00:00 2001 From: Faraphel Date: Sat, 20 Aug 2022 18:08:17 +0200 Subject: [PATCH] calling method is only enabled for constant or env function. Disabled list comprehension. Function from the env. Added a test module for safe_eval too --- assets/language/en.json | 4 +- assets/language/fr.json | 4 +- source/safe_eval/__test__.py | 178 ++++++++++++++++++++++++++++++++++ source/safe_eval/safe_eval.py | 18 +++- 4 files changed, 198 insertions(+), 6 deletions(-) create mode 100644 source/safe_eval/__test__.py diff --git a/assets/language/en.json b/assets/language/en.json index 20d3734..74cbfcb 100644 --- a/assets/language/en.json +++ b/assets/language/en.json @@ -101,6 +101,8 @@ "TEMPLATE_USED": "Template used", "MORE_IN_ERROR_LOG": "More information in the error.log file", "COPY_FUNCTION_FORBIDDEN": "Copying functions is forbidden", - "GET_METHOD_FORBIDDEN": "Using getattr on a method is forbidden" + "GET_METHOD_FORBIDDEN": "Using getattr on a method is forbidden", + "CAN_ONLY_CALL_METHOD_OF_CONSTANT": "You can only call methods on constant", + "CAN_ONLY_CALL_FUNCTION_IN_ENV": "You can only call function from the environment" } } \ No newline at end of file diff --git a/assets/language/fr.json b/assets/language/fr.json index 4e44700..641b19a 100644 --- a/assets/language/fr.json +++ b/assets/language/fr.json @@ -102,6 +102,8 @@ "TEMPLATE_USED": "Modèle utilisé", "MORE_IN_ERROR_LOG": "Plus d'information dans le fichier error.log", "COPY_FUNCTION_FORBIDDEN": "Impossible de copier une fonction", - "GET_METHOD_FORBIDDEN": "Impossible d'utiliser getattr sur une méthode" + "GET_METHOD_FORBIDDEN": "Impossible d'utiliser getattr sur une méthode", + "CAN_ONLY_CALL_METHOD_OF_CONSTANT": "Vous ne pouvez appeler que des méthodes sur des constantes", + "CAN_ONLY_CALL_FUNCTION_IN_ENV": "Vous ne pouvez appeler que des fonctions dans l'environnement" } } \ No newline at end of file diff --git a/source/safe_eval/__test__.py b/source/safe_eval/__test__.py new file mode 100644 index 0000000..5711739 --- /dev/null +++ b/source/safe_eval/__test__.py @@ -0,0 +1,178 @@ +from typing import Callable + +from source.safe_eval import BetterSafeEvalError +from source.safe_eval.safe_eval import safe_eval, SafeEvalException + + +class Object1: + value_int = 1000 + value_str = "test" + value_list = [1, 2, 3, 4, 5] + value_list_rec = [[1, 2], [3, 4]] + + def method(self, value): return value + def __repr__(self): return "repr test" + + +class Object2: + def method2(self, value): return value + sub_obj = Object1() + + +def assertRaise(func: Callable, expected_errors: "type | tuple[type]"): + try: + func() + assert False + except Exception as exc: + assert isinstance(exc, expected_errors) + + +def test_arithmetic(): + assert safe_eval("1 + 5 * 7 ** 2")() == 1 + 5 * 7 ** 2 + + +def test_list(): + assert safe_eval("['string', 10, 0x189, [], (), {}]")() == ['string', 10, 0x189, [], (), {}] + + +def test_slicing(): + assert safe_eval("[1, 2, 3, 4, 5][2]")() == 3 + + +def test_dict(): + assert safe_eval("{'a': 1, 'b': 2, 'c': 3}['a']")() == 1 + + +def test_builtins_method(): + assert safe_eval("','.join(['a', 'b', 'c'])")() == "a,b,c" + + +def test_import(): + assertRaise(lambda: safe_eval("import os")(), SafeEvalException) + + +def test_import_dunder(): + assertRaise(lambda: safe_eval("__import__('os')")(), SafeEvalException) + + +def test_non_lambda_expression(): + assertRaise(lambda: safe_eval("x = 100")(), SafeEvalException) + + +def test_dunder(): + assertRaise(lambda: safe_eval("().__class__.__bases__")(), SafeEvalException) + + +def test_getattr_env(): + assert safe_eval("obj.value_int", env={"obj": Object1()})() == 1000 + + +def test_getvalue_env(): + assert safe_eval("value", env={"value": 1000})() == 1000 + + +def test_setvalue_env(): + assertRaise(lambda: safe_eval("(value := 100)", env={"value": 1000})(), SafeEvalException) + + +def test_getattr_arg(): + assert safe_eval("obj.value_int", args=["obj"])(obj=Object1()) == 1000 + + +def test_getvalue_arg(): + assert safe_eval("value", args=["value"])(value=1000) == 1000 + + +def test_setvalue_arg(): + assertRaise(lambda: safe_eval("(value := 100)", args=["value"])(value=1000), SafeEvalException) + + +def test_assign(): + assert safe_eval("(value := 100)")() == 100 + + +def test_assign_copy_value(): + obj = Object1() + safe_eval("(value := obj.value_list_rec[0], value := [100])", env={"obj": obj})() + assert obj.value_list_rec[0] == [1, 2] + + +def test_assign_copy_func(): + assertRaise(lambda: safe_eval("(value := func)", env={"func": lambda: 'pass'})(), BetterSafeEvalError) + + +def test_assign_copy_method(): + assertRaise(lambda: safe_eval("(value := obj.method)", env={"obj": Object1()})(), BetterSafeEvalError) + + +def test_call_method(): + assertRaise(lambda: safe_eval("obj.method('test')", env={"obj": Object1()})(), SafeEvalException) + + +def test_getattr_value(): + assert safe_eval("getattr(obj, 'value_int')", env={"obj": Object1()})() == 1000 + + +def test_getattr_value_dunder(): + assertRaise(lambda: safe_eval("getattr(obj, '__dict__')", env={"obj": Object1()})(), BetterSafeEvalError) + + +def test_getattr_method_dunder(): + assertRaise(lambda: safe_eval("getattr(obj, 'method')", env={"obj": Object1()})(), BetterSafeEvalError) + + +def test_call_submethod(): + assertRaise(lambda: safe_eval("obj.sub_obj.method('test')", env={"obj": Object2()})(), SafeEvalException) + + +def test_class_new(): + safe_eval("obj_class()", env={"obj_class": Object1})() + + +def test_raise(): + assertRaise(lambda: safe_eval("raise Exception()")(), SafeEvalException) + + +def test_assert(): + assertRaise(lambda: safe_eval("assert True")(), SafeEvalException) + + +def test_del(): + assertRaise(lambda: safe_eval("(x := 10, del x)")(), SyntaxError) + + +def test_import_from(): + assertRaise(lambda: safe_eval("from os import path")(), SafeEvalException) + + +def test_lambda(): + assertRaise(lambda: safe_eval("(lambda x: x ** 2)(10)")(), SafeEvalException) + + +def test_global(): + assertRaise(lambda: safe_eval("(x := 10, global x)")(), SyntaxError) + + +def test_nonlocal(): + assertRaise(lambda: safe_eval("(x := 10, nonlocal x)")(), SyntaxError) + + +def test_class_def(): + assertRaise(lambda: safe_eval("(class c: pass)")(), SyntaxError) + + +def test_list_comprehension(): + # could maybe be authorised ??? + assertRaise(lambda: safe_eval("[x for x in range(10)]")() == [x for x in range(10)], SafeEvalException) + + +def test_list_comprehension_override(): + assertRaise(lambda: safe_eval("[x for x in range(10)]", env={"x": "value"})(), SafeEvalException) + + +def test_list_comprehension_method(): + assertRaise(lambda: safe_eval("[x('test') for x in [obj.method]]", env={"obj": Object1()})(), SafeEvalException) + + +def test_list_slicing(): + assertRaise(lambda: safe_eval("[obj.method][0]('value')", env={"obj": Object1()})(), SafeEvalException) diff --git a/source/safe_eval/safe_eval.py b/source/safe_eval/safe_eval.py index ca5328c..de93c8b 100644 --- a/source/safe_eval/safe_eval.py +++ b/source/safe_eval/safe_eval.py @@ -93,6 +93,17 @@ def safe_eval(template: "TemplateSafeEval", env: "Env" = None, macros: dict[str, args=[node.value], keywords=[], ) + case ast.Call: + if isinstance(node.func, ast.Attribute): # if this is a method + if not isinstance(node.func.value, ast.Constant): # if the method is not on a constant + raise SafeEvalException(_("CAN_ONLY_CALL_METHOD_OF_CONSTANT")) + + elif isinstance(node.func, ast.Name): # if this is a direct function call + if node.func.id not in globals_ | locals_: # if the function is not in env + raise SafeEvalException(_("CAN_ONLY_CALL_FUNCTION_IN_ENV")) + + else: raise SafeEvalException(_("CAN_ONLY_CALL_FUNCTION_IN_ENV")) # else don't allow the function call + # Forbidden type. Some of them can't be accessed with the eval mode, but just in case, still ban them case ( ast.Assign | ast.AugAssign | # Assign should only be done by ":=" with check in eval @@ -102,7 +113,9 @@ def safe_eval(template: "TemplateSafeEval", env: "Env" = None, macros: dict[str, ast.Lambda | ast.FunctionDef | # Defining functions can allow skipping some check ast.Global | ast.Nonlocal | # Changing variables range could cause some issue ast.ClassDef | # Declaring class could maybe allow for dangerous calls - ast.AsyncFor | ast.AsyncWith | ast.AsyncFunctionDef | ast.Await # Just in case + ast.AsyncFor | ast.AsyncWith | ast.AsyncFunctionDef | ast.Await | # Just in case + # comprehension are extremely dangerous since their can associate value + ast.ListComp | ast.SetComp | ast.DictComp | ast.GeneratorExp ): raise SafeEvalException(_("FORBIDDEN_SYNTAX", ' : "', type(node).__name__, '"')) @@ -126,6 +139,3 @@ def safe_eval(template: "TemplateSafeEval", env: "Env" = None, macros: dict[str, lambda_template = eval(compile(expression, "", "eval"), globals_, locals_) self.safe_eval_cache[template_key] = lambda_template # cache the callable for potential latter call return better_safe_eval_error(lambda_template, template=template) - - -# TODO: disable some method and function call. for example, mod_config.path.unlink() is dangerous !