【问题标题】:Using python's eval() vs. ast.literal_eval()使用 python 的 eval() 与 ast.literal_eval()
【发布时间】:2013-02-18 07:29:00
【问题描述】:

我遇到了一些代码的情况,eval() 提出了一个可能的解决方案。现在我以前从未使用过eval(),但是,我发现了大量关于它可能导致的潜在危险的信息。也就是说,我对使用它非常谨慎。

我的情况是我有用户给出的输入:

datamap = input('Provide some data here: ')

datamap 需要是字典。我四处搜索,发现eval() 可以解决这个问题。我认为我可以在尝试使用数据之前检查输入的类型,这将是一种可行的安全预防措施。

datamap = eval(input('Provide some data here: ')
if not isinstance(datamap, dict):
    return

我通读了文档,但我仍然不清楚这是否安全。 eval 是在输入数据后立即评估数据还是在调用 datamap 变量后评估数据?

ast 模块的.literal_eval() 是唯一安全的选择吗?

【问题讨论】:

    标签: python eval abstract-syntax-tree


    【解决方案1】:

    在最近的 Python3 中,ast.literal_eval() 不再解析简单的字符串,而是应该使用 ast.parse() 方法创建一个 AST 然后对其进行解释。

    这是在 Python 3.6+ 中正确使用 ast.parse() 来安全评估简单算术表达式的完整示例。

    import ast, operator, math
    import logging
    
    logger = logging.getLogger(__file__)
    
    def safe_eval(s):
    
        def checkmath(x, *args):
            if x not in [x for x in dir(math) if not "__" in x]:
                raise SyntaxError(f"Unknown func {x}()")
            fun = getattr(math, x)
            return fun(*args)
    
        binOps = {
            ast.Add: operator.add,
            ast.Sub: operator.sub,
            ast.Mult: operator.mul,
            ast.Div: operator.truediv,
            ast.Mod: operator.mod,
            ast.Pow: operator.pow,
            ast.Call: checkmath,
            ast.BinOp: ast.BinOp,
        }
    
        unOps = {
            ast.USub: operator.neg,
            ast.UAdd: operator.pos,
            ast.UnaryOp: ast.UnaryOp,
        }
    
        ops = tuple(binOps) + tuple(unOps)
    
        tree = ast.parse(s, mode='eval')
    
        def _eval(node):
            if isinstance(node, ast.Expression):
                logger.debug("Expr")
                return _eval(node.body)
            elif isinstance(node, ast.Str):
                logger.debug("Str")
                return node.s
            elif isinstance(node, ast.Num):
                logger.debug("Num")
                return node.value
            elif isinstance(node, ast.Constant):
                logger.info("Const")
                return node.value
            elif isinstance(node, ast.BinOp):
                logger.debug("BinOp")
                if isinstance(node.left, ops):
                    left = _eval(node.left)
                else:
                    left = node.left.value
                if isinstance(node.right, ops):
                    right = _eval(node.right)
                else:
                    right = node.right.value
                return binOps[type(node.op)](left, right)
            elif isinstance(node, ast.UnaryOp):
                logger.debug("UpOp")
                if isinstance(node.operand, ops):
                    operand = _eval(node.operand)
                else:
                    operand = node.operand.value
                return unOps[type(node.op)](operand)
            elif isinstance(node, ast.Call):
                args = [_eval(x) for x in node.args]
                r = checkmath(node.func.id, *args)
                return r
            else:
                raise SyntaxError(f"Bad syntax, {type(node)}")
    
        return _eval(tree)
    
    
    if __name__ == "__main__":
        logger.setLevel(logging.DEBUG)
        ch = logging.StreamHandler()
        logger.addHandler(ch)
        assert safe_eval("1+1") == 2
        assert safe_eval("1+-5") == -4
        assert safe_eval("-1") == -1
        assert safe_eval("-+1") == -1
        assert safe_eval("(100*10)+6") == 1006
        assert safe_eval("100*(10+6)") == 1600
        assert safe_eval("2**4") == 2**4
        assert safe_eval("sqrt(16)+1") == math.sqrt(16) + 1
        assert safe_eval("1.2345 * 10") == 1.2345 * 10
    
        print("Tests pass")
    

    【讨论】:

    • 如果我想解析一个 ast.Lambda,比如 safe_eval("lambda x: x * 2") 怎么办?非常感谢
    • 这篇文章专门关于不解析代码的简单算术评估,而不是解析 Python 语法。如果我能做到“lambda x: x * 2”。然后我可能会做“lambda x: format_hdd()”。无论如何要回答您的问题,其中 X 是一个变量,请使用 safe_eval("X * 2".replace("X", "55")) 在我的实际应用程序中,我使用类似 f-string 的语法,例如safe_eval(f"{X} * 2")
    【解决方案2】:

    Python 的 eager 在其评估中,因此eval(input(...)) (Python 3) 将在用户输入到达eval 时立即评估用户输入,无论您之后如何处理数据。因此,这是不安全的,尤其是当您eval 用户输入时。

    使用ast.literal_eval


    例如,在提示符下输入这个可能对您非常不利:

    __import__('os').system('rm -rf /a-path-you-really-care-about')
    

    【讨论】:

      【解决方案3】:

      ast.literal_eval() 只认为 Python 语法的一小部分是有效的:

      提供的字符串或节点只能由以下 Python 文字结构组成:字符串、字节、数字、元组、列表、字典、集合、布尔值和 None

      __import__('os').system('rm -rf /a-path-you-really-care-about') 传递给ast.literal_eval() 会引发错误,但eval() 会很乐意删除您的文件。

      由于您似乎只允许用户输入普通字典,因此请使用 ast.literal_eval()。它可以安全地做您想做的事,仅此而已。

      【讨论】:

        【解决方案4】:

        datamap = eval(input('Provide some data here: ')) 表示您实际评估代码之前您认为它不安全或不安全。它会在调用函数后立即评估代码。另见the dangers of eval

        如果输入不是有效的 Python 数据类型,ast.literal_eval 会引发异常,因此如果不是,则不会执行代码。

        在需要eval 时使用ast.literal_eval。您通常不应该评估文字 Python 语句。

        【讨论】:

        • 这不是 100% 正确的建议,因为任何位运算符(或重载运算符)都会失败。例如。 ast.literal_eval("1 & 1") 会抛出错误,但 eval("1 & 1") 不会。
        • 只是好奇。如果我们期望像 "1 & 1" 这样的东西,我们不应该使用表达式解析器或其他东西吗?
        • @thelinuxer 你还是应该的,是的;您只是无法将ast.literal_eval 用于类似的事情(例如,您可以手动实现解析器)。
        • @DanielvanFlymen - 对我来说,你的例子表明这 好建议。当您不希望对运算符(如&)进行评估时,您可以使用literal_eval。您不能在其中执行任意代码这一事实是一项功能,而不是错误。
        【解决方案5】:

        评估: 这是非常强大的,但如果您接受来自不受信任输入的字符串来评估,这也是非常危险的。假设正在评估的字符串是 "os.system('rm -rf /')" ?它会真正开始删除您计算机上的所有文件。

        ast.literal_eval: 安全地评估包含 Python 文字或容器显示的表达式节点或字符串。提供的字符串或节点只能由以下 Python 文字结构组成:字符串、字节、数字、元组、列表、字典、集合、布尔值、无、字节和集合。

        语法:强>

        eval(expression, globals=None, locals=None)
        import ast
        ast.literal_eval(node_or_string)
        

        示例:

        # python 2.x - doesn't accept operators in string format
        import ast
        ast.literal_eval('[1, 2, 3]')  # output: [1, 2, 3]
        ast.literal_eval('1+1') # output: ValueError: malformed string
        
        
        # python 3.0 -3.6
        import ast
        ast.literal_eval("1+1") # output : 2
        ast.literal_eval("{'a': 2, 'b': 3, 3:'xyz'}") # output : {'a': 2, 'b': 3, 3:'xyz'}
        # type dictionary
        ast.literal_eval("",{}) # output : Syntax Error required only one parameter
        ast.literal_eval("__import__('os').system('rm -rf /')") # output : error
        
        eval("__import__('os').system('rm -rf /')") 
        # output : start deleting all the files on your computer.
        # restricting using global and local variables
        eval("__import__('os').system('rm -rf /')",{'__builtins__':{}},{})
        # output : Error due to blocked imports by passing  '__builtins__':{} in global
        
        # But still eval is not safe. we can access and break the code as given below
        s = """
        (lambda fc=(
        lambda n: [
            c for c in 
                ().__class__.__bases__[0].__subclasses__() 
                if c.__name__ == n
            ][0]
        ):
        fc("function")(
            fc("code")(
                0,0,0,0,"KABOOM",(),(),(),"","",0,""
            ),{}
        )()
        )()
        """
        eval(s, {'__builtins__':{}})
        

        在上面的代码().__class__.__bases__[0] 中只有对象本身。 现在我们实例化了所有的子类,这里我们的主要enter code here目标是从中找到一个名为n的类。

        我们需要来自实例化子类的code 对象和function 对象。这是CPython 访问对象子类并附加系统的另一种方法。

        从 python 3.7 开始,ast.literal_eval() 现在更严格了。不再允许任意数字的加减法。 link

        【讨论】:

        • 我正在使用 python 2.7,我刚刚检查了它在 python 3.x 上的工作正常。我的错我一直在 python 2.7 上尝试它
        • ast.literal_eval("1+1") 在 python 3.7 中不起作用,如前所述,literal_eval 应该仅限于那些少数数据结构的文字。它应该无法解析二进制操作。
        • 您能解释一下您的KABOOM 代码吗?在这里找到它:KABOOM
        • @winklerrr KABOOM 在这里得到了很好的解释:nedbatchelder.com/blog/201206/eval_really_is_dangerous.html
        【解决方案6】:

        我被ast.literal_eval() 卡住了。我在 IntelliJ IDEA 调试器中尝试过,它在调试器输出中不断返回 None

        但后来当我将它的输出分配给一个变量并用代码打印出来时。它工作得很好。分享代码示例:

        import ast
        sample_string = '[{"id":"XYZ_GTTC_TYR", "name":"Suction"}]'
        output_value = ast.literal_eval(sample_string)
        print(output_value)
        

        它的 python 版本 3.6。

        【讨论】:

          【解决方案7】:

          如果您只需要用户提供的字典,可能更好的解决方案是json.loads。主要限制是 json dicts 需要字符串键。您也只能提供文字数据,但literal_eval 也是如此。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2020-09-07
            • 2014-11-10
            • 2016-12-08
            • 1970-01-01
            相关资源
            最近更新 更多