kwutil.util_eval module¶
Defines a safer eval function. See references for related work. It is possible that one of those libraries may be better suited.
We currently vendor code from [evalidate].
References
- exception kwutil.util_eval.RestrictedSyntaxError[source]¶
Bases:
ExceptionAn exception raised by restricted_eval if a disallowed expression is given
- kwutil.util_eval.restricted_eval(expr, max_chars=32, local_dict=None, builtins_passlist=None)[source]¶
A restricted form of Python’s eval that is meant to be slightly safer
- Parameters:
expr (str) – the expression to evaluate
max_char (int) – expression cannot be more than this many characters
local_dict (Dict[str, Any]) – a list of variables allowed to be used
builtins_passlist (List[str] | None) – if specified, only allow use of certain builtins
References
https://realpython.com/python-eval-function/#minimizing-the-security-issues-of-eval
Notes
This function may not be safe, but it has as many mitigation measures that I know about. This function should be audited and possibly made even more restricted. The idea is that this should just be used to evaluate numeric expressions.
Example
>>> from kwutil.util_eval import * # NOQA >>> builtins_passlist = ['min', 'max', 'round', 'sum'] >>> local_dict = {} >>> max_chars = 32 >>> expr = 'max(3 + 2, 9)' >>> result = restricted_eval(expr, max_chars, local_dict, builtins_passlist) >>> expr = '3 + 2' >>> result = restricted_eval(expr, max_chars, local_dict, builtins_passlist) >>> expr = '3 + 2' >>> result = restricted_eval(expr, max_chars) >>> import pytest >>> with pytest.raises(RestrictedSyntaxError): >>> expr = 'max(a + 2, 3)' >>> result = restricted_eval(expr, max_chars, dict(a=3))
- exception kwutil.util_eval.ValidationException[source]¶
Bases:
EvalException
- exception kwutil.util_eval.CompilationException(exc)[source]¶
Bases:
EvalException- exc = None¶
- exception kwutil.util_eval.ExecutionException(exc)[source]¶
Bases:
EvalException- exc = None¶
- class kwutil.util_eval.SafeAST(safenodes=None, addnodes=None, funcs=None, attrs=None)[source]¶
Bases:
NodeVisitorAST-tree walker class.
create whitelist of allowed operations.
- allowed = {}¶
To generate these:
# Get all subclasses of ast.AST recursively nodes = set()
- def recurse(cls):
nodes.add(cls.__name__) for subclass in cls.__subclasses__():
recurse(subclass)
- blocklist = {
# Base classes / abstract or special typing constructs not actual AST nodes ‘AST’, # base class, not a node type itself ‘EnhancedAST’, # not a standard ast node (likely custom or invalid)
# Typing / parameter constructs, not AST nodes ‘ParamSpec’, ‘TypeAlias’, ‘TypeIgnore’, ‘TypeVar’, ‘TypeVarTuple’, ‘Param’, ‘type_param’, ‘type_ignore’,
# Possibly ambiguous or internal helpers (not node classes) ‘AugLoad’, ‘AugStore’,
# Special pseudo nodes or deprecated ‘FunctionType’, # not an AST node ‘Suite’, # does not exist in Python ast
# Others that are unlikely to be AST nodes ‘Div’, # should be ‘Div’ operator but actually no class named Div; it’s ‘Div’ operator type
} recurse(ast.AST) final_list = [n for n in nodes if n not in blocklist and n.lower() != n] print(f’final_list = {ub.urepr(final_list, nl=1)}’)
[ ‘Not’, ‘YieldFrom’, ‘Pass’, ‘BitAnd’, ‘DictComp’, ‘TryStar’, ‘LShift’, ‘Dict’, ‘AsyncWith’, ‘Num’, ‘MatMult’, ‘BinOp’, ‘AsyncFor’, ‘Ellipsis’, ‘MatchClass’, ‘NotIn’, ‘Del’, ‘MatchValue’, ‘GtE’, ‘ClassDef’, ‘Slice’, ‘NotEq’, ‘Constant’, ‘Starred’, ‘UAdd’, ‘Compare’, ‘Break’, ‘FloorDiv’, ‘GeneratorExp’, ‘Assert’, ‘Global’, ‘RShift’, ‘Set’, ‘Match’, ‘Str’, ‘UnaryOp’, ‘While’, ‘Add’, ‘MatchStar’, ‘AugAssign’, ‘Interactive’, ‘MatchSingleton’, ‘Is’, ‘Return’, ‘Expression’, ‘Lt’, ‘Attribute’, ‘Name’, ‘USub’, ‘Call’, ‘Continue’, ‘Await’, ‘Import’, ‘Nonlocal’, ‘BitOr’, ‘ListComp’, ‘Raise’, ‘Bytes’, ‘Mod’, ‘Lambda’, ‘If’, ‘Sub’, ‘Index’, ‘BoolOp’, ‘Module’, ‘MatchAs’, ‘AnnAssign’, ‘And’, ‘MatchSequence’, ‘IsNot’, ‘Delete’, ‘Load’, ‘LtE’, ‘FunctionDef’, ‘Subscript’, ‘List’, ‘FormattedValue’, ‘Invert’, ‘Yield’, ‘ImportFrom’, ‘Expr’, ‘BitXor’, ‘SetComp’, ‘Try’, ‘NameConstant’, ‘Pow’, ‘IfExp’, ‘With’, ‘Mult’, ‘ExtSlice’, ‘NamedExpr’, ‘MatchOr’, ‘ExceptHandler’, ‘For’, ‘Or’, ‘MatchMapping’, ‘In’, ‘Assign’, ‘Store’, ‘Gt’, ‘AsyncFunctionDef’, ‘Tuple’, ‘Eq’, ‘JoinedStr’, ]
- kwutil.util_eval.evalidate(expression, safenodes=None, addnodes=None, funcs=None, attrs=None)[source]¶
Validate expression.
return node if it passes our checks or pass exception from SafeAST visit.
- kwutil.util_eval.safeeval(expression, context={}, safenodes=None, addnodes=None, funcs=None, attrs=None)[source]¶
C-style simplified wrapper, eval() replacement.
- Parameters:
expr (str) – the expression to evaluate
context (dict) – Optional dictionary of variables to make available during evaluation.
safenodes (List[str] | None) – Specify the name of allowed AST nodes, if unspecified a default list is used.
addnodes (List[str] | None) – List of additional AST node names to allow in addition to safenodes.
funcs (List[str]) – list of allowed function names.
attrs (List[str]) – list of allowed attribute names.
- Returns:
the result of the expression
- Return type:
Any
- Raises:
ExecutionException - if the expression fails to execute –
CompilationException - if the expression fails to parse –
ValidationException - if the expression fails safety checks –
Example
>>> from kwutil.util_eval import safeeval >>> safeeval('3 + 2') 5 >>> safeeval('max(3, 2)', addnodes=['Call'], funcs=['max']) 3 >>> safeeval('x * 2', context={'x': 5}) 10 >>> safeeval('len(lst)', context={'lst': [1, 2, 3]}, addnodes=['Call'], funcs=['len']) 3 >>> safeeval('obj.value', context={'obj': type("O", (), {"value": 42})()}, addnodes=['Attribute'], attrs=['value']) 42 >>> import pytest >>> with pytest.raises(ValidationException): ... safeeval('exec("import os")') >>> with pytest.raises(ValidationException): ... safeeval('os.system("ls")', context={'os': __import__('os')}, addnodes=['Call', 'Attribute'], funcs=[])