【问题标题】: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 的了解。

标签: python eval


【解决方案1】:

是的,使用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)

在某些情况下,您必须使用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 很弱,这不是一个明显的做法。

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

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

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

作为脚注,在精神错乱的反社会者手中,它可能效果不佳。但是,当面对精神错乱的反社会用户或管理员时,最好不要一开始就给他们解释 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
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2023-01-05
            • 1970-01-01
            • 1970-01-01
            • 2011-05-14
            • 2013-09-13
            • 2010-09-23
            相关资源
            最近更新 更多