I want to allow my users a way to run very simple Python functions for a project. Of course, eval()
comes to mind, but it is a huge risk. After thinking about it for a time, I realized that most of the functions that a user might need are very rudimentary, similar to the most common excel functions. So I was thinking something along the lines of maintaining a dictionary where the keys are the functions names, and the user can only pick functions which are defined (by me) within that dictionary. So for example:
def add(a, b): return a + b def sum(numbers): result = 0 for number in numbers: result += number return number ... function_map = { 'add': add, 'sum': sum, ... }
Now, if a user defines a line as add(4, 5)
, the result is the expected 9, however, if they define something like foo(4)
, since the key does not exist in my dictionary, an error would be raised. My question is this: how safe is this? Are there any potential vulnerabilities that I am overlooking here?
Advertisement
Answer
You can de-fang eval
somewhat by using appropriate globals
and locals
arguments. For example, this is wat I used in a kind of calculator.
# To make eval() less dangerous by removing access # to built-in functions. _globals = {"__builtins__": None} # But make standard math functions available. _lnames = ( 'acos', 'asin', 'atan', 'ceil', 'cos', 'cosh', 'e', 'log', 'log10', 'pi', 'sin', 'sinh', 'sqrt', 'tan', 'tanh', 'radians' ) _locals = {k: eval('math.' + k) for k in _lnames} value = eval(expr, _globals, _locals)
But you schould probably screen expressions beforehand as well. Reject those that contain import
or eval
or exec
:
if any(j in expr for j in ('import', 'exec', 'eval')): raise ValueError('import, exec and eval are not allowed')
The module linked above also contains the use of ast
to convert Python calculations into LaTeX math expressions. You could also use ast
to build a custom expression evaluator.
Otherwise, here is a small stack-based postfix expression evaluator that I made.
One difference is that I added the number of arguments that each operator needs to the _ops
values, so that I know how many operands to take from the stack.
import operator import math # Global constants {{{1 _add, _sub, _mul = operator.add, operator.sub, operator.mul _truediv, _pow, _sqrt = operator.truediv, operator.pow, math.sqrt _sin, _cos, _tan, _radians = math.sin, math.cos, math.tan, math.radians _asin, _acos, _atan = math.asin, math.acos, math.atan _degrees, _log, _log10 = math.degrees, math.log, math.log10 _e, _pi = math.e, math.pi _ops = { '+': (2, _add), '-': (2, _sub), '*': (2, _mul), '/': (2, _truediv), '**': (2, _pow), 'sin': (1, _sin), 'cos': (1, _cos), 'tan': (1, _tan), 'asin': (1, _asin), 'acos': (1, _acos), 'atan': (1, _atan), 'sqrt': (1, _sqrt), 'rad': (1, _radians), 'deg': (1, _degrees), 'ln': (1, _log), 'log': (1, _log10) } _okeys = tuple(_ops.keys()) _consts = {'e': _e, 'pi': _pi} _ckeys = tuple(_consts.keys()) def postfix(expression): # {{{1 """ Evaluate a postfix expression. Arguments: expression: The expression to evaluate. Should be a string or a sequence of strings. In a string numbers and operators should be separated by whitespace Returns: The result of the expression. """ if isinstance(expression, str): expression = expression.split() stack = [] for val in expression: if val in _okeys: n, op = _ops[val] if n > len(stack): raise ValueError('not enough data on the stack') args = stack[-n:] stack[-n:] = [op(*args)] elif val in _ckeys: stack.append(_consts[val]) else: stack.append(float(val)) return stack[-1]