为什么使用'eval'是一个不好的做法?

188

我使用下面的类来轻松地存储我的歌曲数据。

class Song:
    """The class to store the details of each song"""
    attsToStore=('Name', 'Artist', 'Album', 'Genre', 'Location')
    def __init__(self):
        for att in self.attsToStore:
            exec 'self.%s=None'%(att.lower()) in locals()
    def setDetail(self, key, val):
        if key in self.attsToStore:
            exec 'self.%s=val'%(key.lower()) in locals()

我觉得这比写一个 if/else 块更具可扩展性。然而,我听说 eval 是不安全的。是吗?有什么风险?如何解决我的类中的根本问题(动态设置 self 的属性)而不会引发风险?


51
你是怎么学习到 exec/eval 的,却不知道 setattr 呢? - u0b34a0f6ae
4
我相信是从一篇比较Python和Lisp的文章中,我学习到了eval函数。 - Nikwin
这本应该在一开始就被视为两个不同的问题——解释“eval”的风险,以及展示如何替换这个特定用法。然而,作为一个规范性重复问题,这个问题太重要了,我们无法对此做出太多改变。 - Karl Knechtel
参见:在Python中使用setattr() - Karl Knechtel
8个回答

248

是的,使用eval是一种不好的做法。以下只是其中的一些原因:

  1. 几乎总有更好的方法来实现
  2. 非常危险和不安全
  3. 使调试变得困难
  4. 速度慢

在您的情况下,您可以使用setattr代替:

class Song:
    """The class to store the details of each song"""
    attsToStore=('Name', 'Artist', 'Album', 'Genre', 'Location')
    def __init__(self):
        for att in self.attsToStore:
            setattr(self, att.lower(), None)
    def setDetail(self, key, val):
        if key in self.attsToStore:
            setattr(self, key.lower(), val)

有时候你必须使用 evalexec,但这种情况很少见。在你的情况下使用 eval 是一个不好的做法。我之所以强调不好的做法,是因为 evalexec 经常被用在错误的地方。

回复评论:

看起来有些人不同意在 OP 情况下 eval 是“非常危险和不安全”的说法。这对于此特定情况可能是正确的,但并不一定适用于所有情况。问题是普遍性的,我列出的原因也适用于一般情况。


30
“非常危险和不安全”是错误的表述,其他三个表述非常清晰。请重新排序,把第二个和第四个排在前面。只有当您周围有邪恶的反社会分子试图破坏您的应用程序时,它才会变得不安全。 - S.Lott
77
@S.Lott,安全问题是普遍避免使用 eval/exec 的一个非常重要的原因。许多应用程序,如网站,应该格外小心。以期望用户输入歌曲名称的网站为例,就绑定在迟早会被利用。即使像“Let's have fun.”这样无意的输入也会导致语法错误并暴露漏洞。 - Nadia Alramli
26
用户输入和 eval 没有直接关联。一个从根本上设计不良的应用程序是无论如何都会存在问题的。eval 不比除以零或尝试导入已知不存在的模块更加能引起糟糕设计的根源。eval 并不是不安全的,应用程序才有可能存在安全隐患。 - S.Lott
23
@jeffjose: 事实上,这是根本性的坏/邪恶,因为它将非参数化数据视为代码(这就是为什么XSS、SQL注入和堆栈溢出存在的原因)。 @S.Lott:“只有在你被邪恶的反社会分子包围,并寻找方法来破坏你的应用程序时,它才不安全。”很好,假设你制作了一个名为calc的程序,为了添加数字,它执行print(eval("{} + {}".format(n1, n2)))并退出。然后你使用某个操作系统分发了这个程序。然后有人制作了一个bash脚本,从股票网站获取一些数字并使用calc加起来。爆炸? - L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
78
我不确定为什么Nadia的说法引起了这么大的争议。对我来说,这很简单:eval是一种用于代码注入的向量,并且以一种大多数其他Python函数所没有的危险方式危险。这并不意味着你完全不能使用它,但我认为你应该谨慎地使用它。 - Owen S.
显示剩余11条评论

50

使用eval是一种薄弱的做法,但不是明显的坏习惯。

  1. 它违反了“软件基本原则”。你的源代码不是全部可执行的内容。除了源代码外,还有必须清楚理解的eval参数。因此,它应该作为最后的手段来使用。

  2. 这通常是一个不经思考的设计信号。动态构建源代码几乎没有很好的理由。几乎可以使用委托和其他OO设计技术来完成任何事情。

  3. 它会导致相对较慢的小代码片段的即时编译。这是可以通过使用更好的设计模式来避免的开销。

作为脚注,在精神错乱的社会人士手中,它可能无法发挥良好的作用。然而,当面对精神错乱的用户或管理员时,最好从一开始就不要给他们解释Python。在邪恶的人手中,Python可能成为一种负担;eval并不会增加任何风险。


7
@Owen S. 关键点是这样的。有些人会告诉你,eval 是一种“安全漏洞”。仿佛 Python 本身不只是任何人都可以修改的一堆解释源码一样。当遇到“eval 是安全隐患”的情况时,你只能假设它只是在心理变态者手中成为了安全隐患。普通程序员仅仅修改现有的 Python 源代码并直接导致他们的问题,而不是通过 eval 魔法间接地导致问题。 - S.Lott
18
我可以告诉你为什么我会说eval存在安全漏洞,这与输入的字符串的可信度有关。如果该字符串全部或部分来自外部世界,则如果不小心处理,程序可能遭受脚本攻击。但这是外部攻击者的错乱,而不是用户或管理员的错乱。 - Owen S.
7
如果那个字符串全部或部分来自外界,通常是错误的。这不是一个“小心谨慎”的问题,而是黑白分明的。如果文本来自用户,它永远不能被信任。这与小心谨慎无关,它是绝对不可信的。否则,文本来自开发者、安装程序或管理员,可以被信任。 - S.Lott
9
@OwenS.:对于一串不可信的Python代码,没有可能使其变得可信。我同意你所说的大部分内容,除了“小心谨慎”的那部分。这是一个非常明显的区别。来自外部世界的代码是不可信的。据我所知,无论进行多少次转义或过滤,都无法使其变得安全。如果您有某种可以使代码接受的转义函数,请分享。我认为这样的函数是不可能存在的。例如,“while True:pass”将很难通过某种转义来保证安全。 - S.Lott
2
@OwenS.:“意图作为字符串,而不是任意代码”。这与此无关。这只是一个字符串值,您永远不会通过eval()传递它,因为它是一个字符串。来自“外部世界”的代码无法进行净化。来自外部世界的字符串只是字符串。我不清楚你在说什么。也许您应该提供一个更完整的博客文章并在此处链接到它。 - S.Lott
显示剩余6条评论

28

是的,这是可能的:

使用Python进行黑客攻击:

>>> eval(input())
"__import__('os').listdir('.')"
...........
...........   #dir listing
...........

以下代码将列出在Windows计算机上运行的所有任务。

>>> eval(input())
"__import__('subprocess').Popen(['tasklist'],stdout=__import__('subprocess').PIPE).communicate()[0]"

在 Linux 中:

>>> eval(input())
"__import__('subprocess').Popen(['ps', 'aux'],stdout=__import__('subprocess').PIPE).communicate()[0]"

为什么这样做是不好的/危险的?我不能只是执行相同的Python代码而不使用eval吗? - mkrieger1
3
这是危险的,因为它允许使用不是程序有意编写的源代码的文本作为源代码。这意味着您不能将来自另一个来源(例如Internet下载、Web提交表单、公共亭子上的键盘等)的数据馈送到程序中,而不允许在运行程序的计算机上执行任意代码。这基本上与SQL注入问题相同,但更糟糕,因为它可以访问整个计算机,而不仅仅是数据库。 - Karl Knechtel

24
在这种情况下,是的。而不是
exec 'self.Foo=val'

你应该使用builtin函数setattr

setattr(self, 'Foo', val)

8
其他用户指出了如何更改您的代码以不依赖于eval; 我将为使用eval的合法用例提供一个例子,即在CPython中甚至都可以找到的一个用例:测试
这里是我在 test_unary.py 中发现的一个示例,其中测试是否会引发TypeError(+|-|~)b'a'
def test_bad_types(self):
    for op in '+', '-', '~':
        self.assertRaises(TypeError, eval, op + "b'a'")
        self.assertRaises(TypeError, eval, op + "'a'")

这里明显不是不良实践;你定义输入,仅观察行为。eval在测试时很方便。
查看此搜索以获取对在CPython git存储库上执行的eval的理解;eval用于测试的情况非常普遍。

8
值得注意的是,针对特定的问题,使用eval有几种替代方案:
最简单的方法是使用setattr
def __init__(self):
    for name in attsToStore:
        setattr(self, name, None)

一种不太明显的方法是直接更新对象的__dict__对象。如果您只想将属性初始化为None,则这比上面的方法更不直观。但请考虑以下内容:

def __init__(self, **kwargs):
    for name in self.attsToStore:
       self.__dict__[name] = kwargs.get(name, None)

这使您能够向构造函数传递关键字参数,例如:
s = Song(name='History', artist='The Verve')

它还允许您更明确地使用locals(),例如:

s = Song(**locals())

如果您真的想将None赋值给在locals()中找到名称的属性:

s = Song(**dict([(k, None) for k in locals().keys()]))

为对象提供一组属性的默认值的另一种方法是定义类的__getattr__方法:

def __getattr__(self, name):
    if name in self.attsToStore:
        return None
    raise NameError, name

当以正常方式未找到命名属性时,将调用此方法。这种方法比在构造函数中简单设置属性或更新__dict__略微复杂,但它的优点是只有在存在属性时才创建属性,这可以大大减少类的内存使用。

所有这些的要点是:一般情况下,避免使用eval有很多原因-执行不受您控制的代码的安全问题,无法调试的代码的实际问题等等。但更重要的原因是通常情况下,您不需要使用它。 Python向程序员公开了许多内部机制,因此您很少需要编写编写代码的代码。


3
另一种在Python中可能更(或更少)符合Pythonic风格的方法是:不直接使用对象的__dict__,而是通过继承或作为属性赋予对象一个实际的字典对象。 - Josh Lee
2
“一种不太明显的方法是直接更新对象的 dict 对象。请注意,这将绕过任何描述符(属性或其他)或 __setattr__ 覆盖,这可能会导致意外结果。setattr() 没有这个问题。” - bruno desthuilliers

3

当使用eval()处理用户提供的输入时,您可以使用户启用将代码降为REPL,并提供类似以下内容:

"__import__('code').InteractiveConsole(locals=globals()).interact()"

你可能可以糊弄过去,但通常情况下,你不希望在你的应用程序中使用向量来进行任意代码执行


0
除了@Nadia Alramli的答案之外,由于我是Python的新手,并且渴望检查使用eval会如何影响时间,我尝试了一个小程序,以下是观察结果:
#Difference while using print() with eval() and w/o eval() to print an int = 0.528969s per 100000 evals()

from datetime import datetime
def strOfNos():
    s = []
    for x in range(100000):
        s.append(str(x))
    return s

strOfNos()
print(datetime.now())
for x in strOfNos():
    print(x) #print(eval(x))
print(datetime.now())

#when using eval(int)
#2018-10-29 12:36:08.206022
#2018-10-29 12:36:10.407911
#diff = 2.201889 s

#when using int only
#2018-10-29 12:37:50.022753
#2018-10-29 12:37:51.090045
#diff = 1.67292

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