为啥使用“评估”是一种不好的做法?
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)
在某些情况下,您必须使用eval
或exec
。但它们很少见。在您的情况下使用 eval
肯定是一种不好的做法。我强调的是不好的做法,因为eval
和exec
经常用在错误的地方。
回复 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 源代码,直接导致他们的问题。不是间接通过eval
magic。
好吧,我可以确切地告诉你为什么我会说 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
【讨论】:
以上是关于为啥使用“评估”是一种不好的做法?的主要内容,如果未能解决你的问题,请参考以下文章