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

Posted

技术标签:

【中文标题】为啥使用“评估”是一种不好的做法?【英文标题】:Why is using 'eval' a bad practice?为什么使用“评估”是一种不好的做法? 【发布时间】:2010-12-22 10:06:42 【问题描述】:

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

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 似乎被认为是一种不好的做法并且使用起来不安全。如果是这样,任何人都可以向我解释为什么并告诉我定义上述类的更好方法吗?

【问题讨论】:

你是怎么知道exec/eval却还不知道setattr的? 我相信这是从一篇比较 python 和 lisp 的文章中了解到的,而不是我对 eval 的了解。 【参考方案1】:

是的,使用eval 是一种不好的做法。仅举几个原因:

    几乎总是有更好的方法来做到这一点 非常危险和不安全 使调试变得困难 慢

在您的情况下,您可以改用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 经常用在错误的地方。

回复 cmets:

似乎有些人不同意 eval 在 OP 案例中“非常危险和不安全”。对于这种特定情况,这可能是正确的,但通常并非如此。这个问题很笼统,我列出的原因也适用于一般情况。

【讨论】:

-1:“非常危险和不安全”是错误的。其他三个非常清楚。请重新排序,使 2 和 4 成为前两个。只有当你被邪恶的反社会者包围,他们正在寻找颠覆你的应用程序的方法时,它才是不安全的。 @S.Lott,不安全是避免 eval/exec 的一个非常重要的原因。许多应用程序(如网站)应格外小心。以一个希望用户输入歌曲名称的网站中的 OP 示例为例。它迟早会被利用。即使是无辜的输入,例如:让我们玩得开心。会导致语法错误并暴露漏洞。 @Nadia Alramli:用户输入和eval 彼此无关。一个从根本上设计错误的应用程序就是从根本上错误设计的。 eval 与除以零或尝试导入已知不存在的模块相比,不再是糟糕设计的根本原因。 eval 并非不安全。应用程序不安全。 @jeffjose:实际上,从根本上是坏的/邪恶的,因为它将未参数化的数据视为代码(这就是存在 XSS、SQL 注入和堆栈粉碎的原因)。 @S.Lott:“只有当你被邪恶的反社会者包围,他们正在寻找颠覆你的应用程序的方法时,这才是不安全的。”很酷,假设你编写了一个程序calc,并添加数字,它执行print(eval(" + ".format(n1, n2))) 并退出。现在你用一些操作系统分发这个程序。然后有人制作了一个 bash 脚本,它从股票网站获取一些数字并使用calc 添加它们。繁荣? 我不知道为什么 Nadia 的断言如此有争议。对我来说这似乎很简单:eval 是代码注入的向量,并且在某种程度上是危险的,而大多数其他 Python 函数却没有。这并不意味着你根本不应该使用它,但我认为你应该明智地使用它。【参考方案2】:

使用eval 很弱,这不是一个明显的做法。

    它违反了“软件基本原则”。您的来源不是可执行文件的总和。除了你的来源,还有eval的论据,一定要清楚明白。因此,它是不得已的工具。

    这通常是粗心设计的标志。动态构建的动态源代码很少有充分的理由。几乎任何事情都可以通过委托和其他 OO 设计技术来完成。

    它会导致小段代码的动态编译相对较慢。可以通过使用更好的设计模式来避免开销。

作为脚注,在精神错乱的反社会者手中,它可能效果不佳。但是,当面对精神错乱的反社会用户或管理员时,最好不要一开始就给他们解释 Python。在真正邪恶的人手中,Python 可能是一种负担; eval 根本不会增加风险。

【讨论】:

@Owen S. 重点是这个。人们会告诉你eval 是某种“安全漏洞”。好像 Python——它本身——不仅仅是一堆任何人都可以修改的解释源。当面对“评估是一个安全漏洞”时,你只能假设它是反社会人士手中的一个安全漏洞。普通程序员只是修改现有的 Python 源代码,直接导致他们的问题。不是间接通过evalmagic。 好吧,我可以确切地告诉你为什么我会说 eval 是一个安全漏洞,它与作为输入的字符串的可信度有关。如果该字符串全部或部分来自外部世界,如果您不小心,您的程序可能会受到脚本攻击。但这是外部攻击者的错乱,而不是用户或管理员的错乱。 @OwenS.:“如果该字符串全部或部分来自外部世界” 通常是错误的。这不是一件“小心”的事情。它是黑白的。如果文本来自用户,则它永远不会被信任。护理并不是其中的一部分,它绝对是不可信的。否则,文本来自开发人员、安装人员或管理员,并且可以信任。 @OwenS.: 不可能转义一串不受信任的 Python 代码,使其变得可信任。我同意你所说的大部分内容,除了“小心”部分。这是一个非常清晰的区别。来自外部世界的代码是不可信的。 AFAIK,没有任何转义或过滤可以清理它。如果您有某种可以使代码可接受的转义功能,请分享。我不认为这样的事情是可能的。例如,while True: pass 很难通过某种转义来清理。 @OwenS.:“旨在作为字符串,而不是任意代码”。那是无关的。这只是一个字符串值,你永远不会通过eval(),因为它是一个字符串。无法清理来自“外部世界”的代码。来自外部世界的字符串只是字符串。我不清楚你在说什么。也许您应该在此处提供更完整的博客文章和链接。【参考方案3】:

在这种情况下,是的。而不是

exec 'self.Foo=val'

你应该使用builtin函数setattr

setattr(self, 'Foo', val)

【讨论】:

【参考方案4】:

是的,它是:

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

【讨论】:

【参考方案5】:

值得注意的是,对于所讨论的具体问题,使用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 向程序员公开了如此多的内部机制,以至于您很少真正需要编写代码来编写代码。

【讨论】:

另一种可以说是更多(或更少)Pythonic的方式:不是直接使用对象的__dict__,而是通过继承或作为属性给对象一个实际的字典对象。 “一种不太明显的方法是直接更新对象的 dict 对象” => 请注意,这将绕过任何描述符(属性或其他)或 __setattr__ 覆盖,这可能导致意想不到的结果。 setattr() 没有这个问题。【参考方案6】:

其他用户指出如何更改您的代码以不依赖于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 便于测试。

Take a look at this search for eval,在 CPython git 存储库上执行; eval 测试被大量使用。

【讨论】:

【参考方案7】:

eval() 用于处理用户提供的输入时,您可以让用户Drop-to-REPL 提供如下内容:

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

您可能会侥幸成功,但通常您不希望在应用程序中使用 arbitrary code execution 的向量。

【讨论】:

【参考方案8】:

除了@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

【讨论】:

以上是关于为啥使用“评估”是一种不好的做法?的主要内容,如果未能解决你的问题,请参考以下文章

为啥单身人士被认为是一种不好的做法? [复制]

从后台线程更新 UI 是一种不好的做法,为啥? [关闭]

为啥向下转换在 C++ 中是一种不好的做法,而不是在另一种语言中呢? [关闭]

使用动态是不是被认为是一种不好的做法?

在 C 中使用 short 是一种不好的做法

使用“?”是一种不好的做法。在 Racket 方法名称中?