使用Python的eval()和ast.literal_eval()的区别

285

我有一个和代码相关的情况,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()是唯一安全的选择吗?
6个回答

304

datamap = eval(input('提供一些数据: ')) 的意思是,你需要在判断代码是否安全之前先对它进行求值。它会在函数被调用时立即对代码进行求值。另请参见 eval的危险性

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

每当你需要使用eval时,请使用ast.literal_eval。通常情况下,你不应该评估Python语句。


43
这个建议并不完全正确,因为任何位运算符(或重载运算符)都将失败。例如,ast.literal_eval("1 & 1") 将会抛出错误,而 eval("1 & 1") 不会。 - Daniel van Flymen
1
只是好奇,如果我们期望类似于“1&1”的内容,我们不应该使用表达式解析器或类似的东西吗? - thelinuxer
1
@thelinuxer 你仍然应该这样做,是的;只是你不能使用ast.literal_eval来实现这样的功能(例如,你可以手动实现一个解析器)。 - Volatility
16
对我来说,你的例子表明这确实是好建议。当你不想让操作符(比如 &)被计算时,你可以使用 literal_eval。无法在那里放置任意代码以执行是一种特性而不是错误。 - Ken Williams

158

ast.literal_eval()仅考虑Python语法的一个小子集是有效的:

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

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

由于看起来您只允许用户输入普通字典,请使用ast.literal_eval()。它安全地执行您想要的操作,而不会做任何其他操作。


87

eval: 这是非常强大的功能,但如果你接受来自不可信输入的字符串进行评估,也是非常危险的。假设被评估的字符串是“os.system('rm -rf /')”?它将开始删除计算机上的所有文件。

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

语法:

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] 仅是对象本身。现在,我们实例化了所有的子类,这里我们的主要目标是从中找到一个名为n的类。
我们需要从实例化的子类中获取code对象和function对象。这是一种从CPython访问对象子类并附加系统的替代方法。
从python 3.7开始,ast.literal_eval()现在更加严格。不再允许任意数字的加减。 链接

1
我正在使用Python 2.7,刚刚检查了它在Python 3.x上的运行情况良好。我的错,我一直在尝试在Python 2.7上运行它。 - Mourya
4
在Python 3.7中,ast.literal_eval("1+1")无法工作,并且像之前说过的那样,literal_eval 应该仅限于那些少量数据结构的字面值。它不应该能够解析二进制操作。 - Sesshu
1
请问您能解释一下您的 KABOOM 代码吗?我在这里找到了它:KABOOM - winklerrr
3
@winklerrr 在这里很好地解释了 KABOOM:https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html - Elijas Dapšauskas
在 Python 3.10 上,ast.literal_eval("1+1") 抛出 ValueError: malformed node or string on line 1 - adrin

56
Python的求值是急切的,所以eval(input(...)) (Python 3)将会在碰到eval时立即计算用户输入的内容,无论之后你对这个数据做了什么。因此,这是不安全的,尤其是当你eval用户输入时。使用ast.literal_eval代替。
作为一个例子,如果在提示符处输入这个内容,可能会对您非常不利:
__import__('os').system('rm -rf /a-path-you-really-care-about')

14
在最近的Python3��,ast.literal_eval()“不再解析简单字符串”,而是应该使用ast.parse()方法创建AST然后进行解释。
* 更新(2023年第1季度):我有时会收到关于在这种情况下“简单字符串”的含义的评论。在阅读了当前状态的相关信息后,我添加了此更新以尝试解决这个问题。

我写了这篇答案很久以前,当时我使用了“简单字符串”这个短语,但不幸的是我不记得当时的来源,但它可能已经过时了,但确实在某个时间点上,这种方法期望的不是一个字符串。因此,在当时,这是对Python 2的引用,而这一事实在Python 3中略有变化,但仍存在一些限制。然后在某个时候,我将代码从Py2更新为Py3语法,导致了混淆。

我希望这个答案仍然是一个完整的示例,说明如何编写一个安全的解析器,可以在作者的控制下评估任意表达式,然后通过清理每个参数来解释不受控制的数据。欢迎评论,因为我仍然在现场项目中使用类似的东西!

所以现在唯一的更新是,对于非常简单的Python表达式,ast.iteral_eval(str: statements)如果我理解正确,现在被认为是安全的。

这个答案是我希望仍然是一个工作的最小示例,展示如何实现类似于ast.literal_eval(str: statements)的东西,以支持更多的函数、方法和数据类型,但仍然以一种简单的方式来考虑安全性。我相信还有其他方法,但那将与本问题的主题无关。


这是一个完整的示例,展示如何在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"),该怎么办?非常感谢。 - Menglong Li
这篇文章特别讲述了简单算术运算的评估,而不是解析Python语法。如果我可以做到"lambda x: x * 2",那么我可能可以做到"lambda x: format_hdd()"。无论如何回答你的问题,在X是一个变量的情况下,使用safe_eval("X * 2".replace("X", "55"))。在我的实际应用中,我使用类似f-string的语法,例如safe_eval(f"{X} * 2")。 - Jay M
你说的“不再解析简单字符串”是什么意思?据我所知,它的行为在Python版本之间没有改变,比如如果你看一下3.0版本的文档,描述基本上是一样的。它从来就不允许解析任意表达式,如果你是指这个的话,而且它也从来没有停止解析字符串,如果你是指这个的话。 - wjandrea
@JayM 但是“simple strings”是什么意思呢? - wjandrea
正如我所说的,我记得那是我遇到的解释中的话,说明为什么我的旧代码必须重新编写。我没有链接该参考资料,因为我认为这种更改是众所周知的。直到我需要解析用户提供的 Verilog 文件,我才多年未使用 AST。显然,我不希望用户在宏中放置任何可能被误用的内容。旧代码可能来自 Py2 或甚至晚期的 Py1。我真的不记得了。 - Jay M
显示剩余7条评论

9
如果你只需要一个用户提供的字典,可能更好的解决方案是使用 json.loads。 主要限制是JSON字典(“对象”)需要字符串键。 此外,你只能提供字面数据,但这也适用于ast.literal_eval

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接