OK. I know the experts have spoken and you should not ever use python's eval()
on untrusted data, ever. I'm not smarter than the rest of the world, and shouldn't even try this. But! I'm going to, anyhow.
My basic problem is that I'm looking to write a little calculator evaluator program that'll take untrusted input, using a subset of python's syntax. I know: use ply or pyparsing and write a parser and there we go. Screwing around with passing globals and locals to eval()
will not do the trick.
All the approaches I've seen (and been leery about) try to enumerate evil. Here, I'm trying to enumerate good -- get an AST, allow only a few node types, and then verify that any calls are to one of a set of whitelisted functions. Here's a mini-implementation (and a gist):
import ast
import math
SAFE_FX = {
'exp': math.exp,
}
SAFE_NODES = set(
(ast.Expression,
ast.Num,
ast.Call,
ast.Name,
ast.Load,
ast.BinOp,
ast.Add,
ast.Sub,
ast.Mult,
ast.Div,)
)
class CleansingNodeVisitor(ast.NodeVisitor):
def generic_visit(self, node):
if type(node) not in SAFE_NODES:
raise Exception("%s not in SAFE_NODES" % type(node))
super(CleansingNodeVisitor, self).generic_visit(node)
def visit_Call(self, call):
if call.func.id not in SAFE_FX:
raise Exception("Unknown function: %s" % call.func.id)
def my_safe_eval(s):
tree = ast.parse(s, mode='eval')
cnv = CleansingNodeVisitor()
cnv.visit(tree)
compiled = compile(tree, s, "eval")
return(eval(compiled, SAFE_FX))
So, my_safe_eval('2*(4+exp(1.3))')
works, while my_safe_eval('[].__class__')
tricks and my_safe_eval('open("/something/evil")')
is likewise forbidden -- without forbidding __builtins__
or __locals__
or anything.
I... I think this works. Am I mad?