为什么使用“评估”是一种不好的做法?


138

我正在使用以下课程轻松存储我的歌曲的数据。

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似乎被认为是不良做法,使用不安全。如果是这样,有人可以向我解释原因并向我展示定义上述类的更好方法吗?


40
exec/eval是怎么知道的,仍然不知道setattr
u0b34a0f6ae 2009年

3
我相信这是来自一篇比较python和lisp的文章,而不是我了解到的eval。
Nikwin,2009年

Answers:


194

是的,使用eval是一种不好的做法。仅出于以下几个原因:

  1. 几乎总有一种更好的方法
  2. 非常危险和不安全
  3. 使调试困难

您可以使用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)

编辑:

在某些情况下,您必须使用eval或exec。但是它们很少见。当然,在您的情况下使用eval是一个不好的做法。我要强调不好的做法,因为eval和exec经常在错误的地方使用。

编辑2:

似乎有些不同意,在OP案件中,评估是“非常危险和不安全的”。对于这种特定情况,这可能是正确的,但一般而言并非如此。问题是一般性的,我列出的理由也适用于一般性情况。

编辑3: 重新排序的点1和4


22
-1:“非常危险和不安全”是错误的。其他三个非常清楚。请重新排列它们,使2和4为前两个。只有当您被邪恶的社会变态者包围着,他们正在寻找颠覆您的应用程序的方式时,这才是不安全的。
S.Lott

51
@ S.Lott,通常,不安全是避免使用eval / exec的重要原因。网站等许多应用程序应格外小心。以一个希望用户输入歌曲名称的网站中的OP为例。它势必迟早会被利用。甚至是无辜的输入,例如:让我们玩得开心。将导致语法错误并暴露该漏洞。
Nadia Alramli 09年

17
@Nadia Alramli:用户输入,eval彼此无关。从根本上设计错误的应用程序从根本上设计错误。 eval导致设计不良的根本原因不只是被零除或试图导入一个不存在的模块。 eval不是不安全的。应用程序是不安全的。
S.Lott

17
@jeffjose:实际上,从根本上讲,它是不好的/邪恶的,因为它将未参数化的数据视为代码(这就是为什么存在XSS,SQL注入和堆栈粉碎的原因)。@ S.Lott:“只有当您被邪恶的社会变态者包围着,他们正在寻找颠覆您的应用程序的方式时,这才是不安全的。” 太酷了,所以说您制作了一个程序calc,并添加了要执行print(eval("{} + {}".format(n1, n2)))和退出的数字。现在,您通过某些OS分发该程序。然后有人制作了一个bash脚本,该脚本从一个股票站点获取一些数字,并使用来添加它们calc。繁荣?
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

57
我不确定纳迪亚的主张为何如此有争议。在我看来,这很简单:eval是代码注入的向量,并且以大多数其他Python函数都不是的方式是危险的。这并不意味着您根本不应该使用它,但是我认为您应该明智地使用它。
Owen S.

32

使用eval是很弱的,不是一个明显的习惯。

  1. 它违反了“软件基本原理”。您的来源不是可执行文件的总和。除了您的资料来源外,还eval必须清楚地了解到的参数。因此,它是万不得已的工具。

  2. 通常,这是经过漫长设计的标志。动态构建动态源代码的理由很少。委托和其他OO设计技术几乎可以完成任何事情。

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

作为注脚,在精神错乱的社会主义者的手中,这可能效果不佳。但是,当遇到精神错乱的用户或管理员时,最好不要首先让他们理解Python。在真正的邪恶之手,Python可以承担责任。eval完全不会增加风险。


7
@OwenS。关键是这一点。人们会告诉您这eval是某种“安全漏洞”。好像Python(本身)不仅是一堆可以解释的解释源。当面对“评估是一个安全漏洞”时,您只能假定这是社会病患者手中的一个安全漏洞。普通程序员只是修改现有的Python源代码并直接导致他们的问题。不是间接通过eval魔法。
S.Lott

14
好吧,我可以确切地告诉您为什么我要说eval是一个安全漏洞,这与作为输入给出的字符串的可信赖性有关。如果该字符串全部或部分来自外界,那么如果您不小心的话,就有可能对您的程序进行脚本攻击。但这是外部攻击者(而不是用户或管理员)的混乱。
Owen S.

6
@OwenS .:“如果该字符串全部或部分来自外部世界”,通常为假。这不是“小心”的事情。黑色和白色。如果文本来自用户,则永远不会被信任。护理并不是真正的一部分,它绝对不可信任。否则,文本来自开发人员,安装人员或管理员,并且可以信任。
S.Lott 2012年

8
@OwenS .:无法逃避一串使它可信任的不受信任的Python代码。我同意您所说的大部分内容,但“谨慎”部分除外。这是一个非常清晰的区别。来自外界的代码是不可信的。AFAIK,没有任何转义或过滤功能可以清除它。如果您具有某种可以使代码可接受的转义功能,请分享。我认为这样的事情是不可能的。例如while True: pass,使用某种逃逸手段很难清除。
S.Lott 2012年

2
@OwenS .:“打算作为字符串,而不是任意代码”。没关系。那只是一个字符串值,您永远都不会通过eval()它,因为它是一个字符串。“外部世界”中的代码无法清除。来自外界的字符串仅仅是字符串。我不清楚你在说什么。也许您应该提供更完整的博客文章并在此处链接。
S.Lott 2012年

23

在这种情况下,可以。代替

exec 'self.Foo=val'

您应该使用内置函数setattr

setattr(self, 'Foo', val)

16

是的:

使用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]"

7

值得注意的是,对于有问题的特定问题,可以使用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

如果无法以常规方式找到named属性,则调用此方法。这种方法比简单地在构造函数中设置属性或更新的方式要简单一些__dict__,但是它的优点是除非存在该属性,否则不实际创建该属性,这样可以大大减少类的内存使用量。

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


1
可以说是(或更少)Pythonic的另一种方式:__dict__通过继承或作为属性,而不是直接使用该对象,而应给该对象一个实际的字典对象。
Josh Lee,2009年

1
“不太明显的方法是直接更新对象的dict对象” =>请注意,这将绕过任何描述符(属性或其他)或__setattr__重写,这可能会导致意外结果。setattr()没有这个问题。
bruno desthuilliers

5

其他用户指出了如何可以更改不依赖的代码eval; 我将提供一个使用的合法用例eval,即使在CPython中也可以找到一个用例:testing

这是我在test_unary.py其中测试是否(+|-|~)b'a'引发的一个示例TypeError

def test_bad_types(self):
    for op in '+', '-', '~':
        self.assertRaises(TypeError, eval, op + "b'a'")
        self.assertRaises(TypeError, eval, op + "'a'")

显然,这里的用法不是坏习惯;您定义输入,仅观察行为。eval方便测试。

看看这个搜索在eval,在CPython的Git仓库中进行; 大量使用eval进行测试。


2

什么时候 eval()用于处理用户提供的输入时,您使用户能够拖放到提供以下内容:

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

您可以摆脱它,但是通常您不希望向量在您的应用程序中执行任意代码


1

除了@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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.