Reputation: 8722
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?
Upvotes: 3
Views: 171
Reputation: 43495
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]
Upvotes: 3