Skip to content
Advertisement

Compiling and executing simple user defined code in Python

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]
User contributions licensed under: CC BY-SA
2 People found this is helpful
Advertisement