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: Exception

An 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.EvalException[source]

Bases: Exception

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: NodeVisitor

AST-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’, ]

generic_visit(node)[source]

Check node, raise exception if node is not in whitelist.

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=[])