Simple and safe evaluator

Matimus mccredie at gmail.com
Wed Jun 11 20:05:22 EDT 2008


On Jun 11, 4:38 pm, bvdp <b... at mellowood.ca> wrote:
> I'm finding my quest for a safe eval() quite frustrating :)
>
> Any comments on this: Just forget about getting python to do this and,
> instead, grab my set of values (from a user supplied text file) and call
> an external program like 'bc' to do the dirty work. I think that this
> would avoid someone from embedding os.system("rm ...") in what I thought
> would be a math expression and having it maybe do damage? Perhaps I'm
> getting too paranoid in my old age.
>
> I guess this would slow things down a bit, but that is not a big
> concern. Bigger concern would be that I'm not sure if 'bc' or whatever
> is guaranteed to be on other platforms than *nix. And if I want to be
> really paranoid, I could worry that someone had planted a bad 'bc' on
> the target.

The solution I posted should work and is safe. It may not seem very
readable, but it is using Pythons internal parser to parse the passed
in string into an abstract symbol tree (rather than code). Normally
Python would just use the ast internally to create code. Instead I've
written the code to do that. By avoiding anything but simple operators
and literals it is guaranteed safe.

If you only want numbers you can even remove a bunch of code:
[code]
import _ast

class SafeEvalError(Exception):
    pass

class UnsafeCode(SafeEvalError):
    pass

def num_eval(text):
    "similar to eval, but only works on numerical values."
    ast = compile(text, "<string>", 'eval', _ast.PyCF_ONLY_AST)
    return _traverse(ast.body)

def _traverse(ast):

    if isinstance(ast, _ast.Num):
        return ast.n
    elif isinstance(ast, _ast.Expr):
        return _traverse(ast.value)
    elif isinstance(ast, _ast.BinOp):
        if isinstance(ast.op, _ast.Add):
            return _traverse(ast.left) + _traverse(ast.right)
        elif isinstance(ast.op, _ast.Sub):
            return _traverse(ast.left) - _traverse(ast.right)
        elif isinstance(ast.op, _ast.Div):
            return _traverse(ast.left) / _traverse(ast.right)
        elif isinstance(ast.op, _ast.FloorDiv):
            return _traverse(ast.left) // _traverse(ast.right)
        elif isinstance(ast.op, _ast.Mod):
            return _traverse(ast.left) % _traverse(ast.right)
        elif isinstance(ast.op, _ast.Mult):
            return _traverse(ast.left) * _traverse(ast.right)
        elif isinstance(ast.op, _ast.Pow):
            return _traverse(ast.left) ** _traverse(ast.right)
        elif isinstance(ast.op, _ast.BitAnd):
            return _traverse(ast.left) & _traverse(ast.right)
        elif isinstance(ast.op, _ast.BitOr):
            return _traverse(ast.left) | _traverse(ast.right)
        elif isinstance(ast.op, _ast.BitXor):
            return _traverse(ast.left) ^ _traverse(ast.right)
        elif isinstance(ast.op, _ast.LShift):
            return _traverse(ast.left) << _traverse(ast.right)
        elif isinstance(ast.op, _ast.RShift):
            return _traverse(ast.left) >> _traverse(ast.right)
    elif isinstance(ast, _ast.UnaryOp):
        if isinstance(ast.op, _ast.Invert):
            return ~_traverse(ast.operand)
        elif isinstance(ast.op, _ast.USub):
            return -_traverse(ast.operand)
        elif isinstance(ast.op, _ast.UAdd):
            return +_traverse(ast.operand)

    raise UnsafeCode()
[/code]

To use:
print num_eval("1 + 44 / 3")



More information about the Python-list mailing list