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

This commit is contained in:
Faraphel 2022-08-20 18:08:17 +02:00
parent 601342ab20
commit 92f32aec80
4 changed files with 198 additions and 6 deletions

View file

@ -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"
}
}

View file

@ -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"
}
}

View file

@ -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)

View file

@ -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, "<safe_eval>", "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 !