Evaluate math equations from unsafe user input in Python

前端 未结 2 1776
粉色の甜心
粉色の甜心 2020-12-03 02:42

I have a website where the user enters math equations (expressions) and then those equations are evaluated against data (constants) provided by the website. The math operati

2条回答
  •  没有蜡笔的小新
    2020-12-03 03:24

    Disclaimer: I'm the Alexer mentioned in the code in the other answer. To be honest, I kind of suggested the bytecode parsing approach only half-jokingly, since I happened to have 99% of the code lying around for an unrelated project and so could whip together a POC in like a couple of minutes. That said, there shouldn't be anything wrong with it, per se; it's just that it's a more complex machinery that is needed for this task. In fact, you should be able to get away with just disassembling the code [checking the opcodes against a whitelist], checking that the constants and names are valid, and executing it with plain, evil eval after that. You should just lose the ability to insert paranoid extra checks all over the execution. (Another disclaimer: I still wouldn't feel comfortable enough to do it with eval)

    Anyway, I had a boring moment, so I wrote some code to do this the smart way; using the AST instead of the bytecode. It's just an extra flag to compile(). (Or just ast.parse(), since you'll want the types from the module anyway)

    import ast
    import operator
    
    _operations = {
            ast.Add: operator.add,
            ast.Sub: operator.sub,
            ast.Mult: operator.mul,
            ast.Div: operator.div,
            ast.Pow: operator.pow,
    }
    
    def _safe_eval(node, variables, functions):
            if isinstance(node, ast.Num):
                    return node.n
            elif isinstance(node, ast.Name):
                    return variables[node.id] # KeyError -> Unsafe variable
            elif isinstance(node, ast.BinOp):
                    op = _operations[node.op.__class__] # KeyError -> Unsafe operation
                    left = _safe_eval(node.left, variables, functions)
                    right = _safe_eval(node.right, variables, functions)
                    if isinstance(node.op, ast.Pow):
                            assert right < 100
                    return op(left, right)
            elif isinstance(node, ast.Call):
                    assert not node.keywords and not node.starargs and not node.kwargs
                    assert isinstance(node.func, ast.Name), 'Unsafe function derivation'
                    func = functions[node.func.id] # KeyError -> Unsafe function
                    args = [_safe_eval(arg, variables, functions) for arg in node.args]
                    return func(*args)
    
            assert False, 'Unsafe operation'
    
    def safe_eval(expr, variables={}, functions={}):
            node = ast.parse(expr, '', 'eval').body
            return _safe_eval(node, variables, functions)
    
    if __name__ == '__main__':
            import math
    
            print safe_eval('sin(a*pi/b)', dict(a=1, b=2, pi=math.pi), dict(sin=math.sin))
    

    The same thing applies to this as to the bytecode version; if you check the operations against a whitelist and check that the names and values are valid, you should be able to get away with calling eval on the AST. (But again, I still wouldn't do it. Because paranoid. And paranoia is good when eval is concerned)

提交回复
热议问题