Python中'eval'函数用于列表反序列化的安全性问题

7

在这种情况下,是否存在任何可能发生安全漏洞的风险:

eval(repr(unsanitized_user_input), {"__builtins__": None}, {"True":True, "False":False})

其中unsanitized_user_input是一个字符串对象。该字符串由用户生成,可能很恶劣。假设我们的Web框架没有出现问题,那么它就是Python内置的一个真实的字符串实例。

如果这很危险,我们能否对输入做些什么来使其安全?

我们绝对不希望执行字符串中包含的任何内容。

另请参阅:

更大的上下文(我认为)与问题无关,我们有成千上万个这样的问题:

repr([unsanitized_user_input_1,
      unsanitized_user_input_2,
      unsanitized_user_input_3,
      unsanitized_user_input_4,
      ...])

在某些情况下,需要嵌套:

repr([[unsanitized_user_input_1,
       unsanitized_user_input_2],
      [unsanitized_user_input_3,
       unsanitized_user_input_4],
       ...])

这些值本身通过 repr() 转换为字符串,存储在持久化存储中,最终通过 eval 读取回内存。

与 pickle 和 simplejson 相比,eval 更快地对持久化存储中的字符串进行反序列化。解释器是 Python 2.5 版本,因此不可用 json 和 ast。不允许使用 C 模块,也不允许使用 cPickle。


你为什么不使用pickle或者其他更简单的东西呢? - S.Lott
我也很好奇为什么首先要对数据进行repr处理。 - Nick Johnson
数据被repr'd以将嵌套列表结构更改为字符串,以便可以对其进行压缩并存储在blob属性中。 - gravitation
问一个显而易见的问题,序列化/反序列化时间真的是瓶颈吗?还是你在进行过早的优化? - Nick Johnson
很好的观察,尼克。目前应用程序的用户数量并不是瓶颈,实际的 CPU 使用率非常低。但是,在我的上一个应用程序出现负载问题后,我已经测试了1000个用户,不幸的是,序列化在那一点上确实成为了瓶颈。然而,可能永远不会有那么多真正的用户,这总是一个猜测游戏。 - gravitation
显示剩余3条评论
5个回答

19

这确实很危险,最安全的替代方法是使用ast.literal_eval(请参见标准库中的ast模块)。当然,在评估结果AST之前,您可以构建和修改一个ast以提供变量的评估等功能(当它们被降级为文字时)。

eval的可能漏洞始于任何它能够获取到的对象(在这里是True),然后通过.__class_访问对象的类型对象等,一直到object,然后获取其子类...基本上,它可以访问到任何对象类型并造成严重破坏。我可以更具体地说明,但我宁愿不在公共论坛上做出来(这个漏洞是众所周知的,但考虑到仍有多少人忽略它,向想要成为脚本小子的人揭示它可能会使情况变得更糟...只需避免对未经过滤的用户输入进行eval,从而过上幸福的生活!-)。


8
如果您能证明 unsanitized_user_input 是 Python 内置的 str 实例,并且没有被篡改,那么这种方式总是安全的。实际上,即使没有所有这些额外的参数,它也是安全的,因为对于所有这样的字符串对象,eval(repr(astr)) = astr。您输入一个字符串,您得到一个字符串。您所做的只是转义和取消转义。
这让我想到,eval(repr(x)) 不是您想要的东西——除非有人给您一个看起来像字符串但实际上不是字符串的 unsanitized_user_input 对象,否则永远不会执行任何代码,但这是另一个问题——除非您正在尝试以最慢的方式复制一个字符串实例 :D。

1
这完全正确;我绝对不希望字符串中的任何内容被执行。如果我呈现更大的上下文,这样做的原因会更有意义,但我试图简化问题的情境。 - gravitation

5
按照您的描述,技术上来说,评估 repred 字符串是安全的,但我仍然建议避免这样做,因为这可能会引起麻烦:
- 可能存在一些奇怪的角落情况,您假设只存储了 repred 字符串(例如,错误/进入存储的不是立即 repr 的不同路径)变成了代码注入漏洞,在其他情况下可能无法利用。 - 即使现在一切都很好,假设也可能会发生变化,并且未经过滤的数据可能会被某个不知道 eval 代码的人存储在该字段中。 - 您的代码可能会被重复使用(或更糟的是,复制+粘贴)到您没有考虑到的情况中。
正如 Alex Martelli 指出的,在 Python2.6 及更高版本中,有 ast.literal_eval,它可以安全地处理字符串和其他简单数据类型(如元组)。这可能是最安全和最完整的解决方案。

另外一种可能性是使用 string-escape 编解码器。这比 eval 快得多(根据 timeit 大约快 10 倍),在比 literal_eval 更早的版本中可用,并且应该可以满足您的需求:

>>> s = 'he\nllo\' wo"rld\0\x03\r\n\tabc'
>>> repr(s)[1:-1].decode('string-escape') == s
True

([1:-1]用于去除外部引号repr添加。)

3

一般来说,你不应该允许任何人发布代码。

所谓的“有偿专业程序员”已经很难编写真正可用的代码了。

接受匿名公众提交的代码——没有正式QA的帮助——是最糟糕的情况。

没有良好、扎实的正式QA的专业程序员几乎会把任何网站搞砸。事实上,我正在反向工程一些由有偿专业人员编写的难以置信糟糕的代码。

允许一个非专业人员——没有QA的限制——发布代码的想法真的很可怕。


1
repr([unsanitized_user_input_1,
      unsanitized_user_input_2,
      ...

... unsanitized_user_input is a str object

您不应该将字符串序列化以将其存储在数据库中。

如果这些都是字符串,正如您所提到的 - 为什么不能只将字符串存储在db.StringListProperty中?

嵌套条目可能会更加复杂,但为什么会出现这种情况?当您不得不使用eval从数据库获取数据时,您可能正在做一些错误的事情。

您不能将每个unsanitized_user_input_x作为自己的db.StringProperty行存储,并通过引用字段对它们进行分组吗?

这两者中的任何一种可能都不适用,因为我不知道您想要实现什么,但我的观点是 - 您不能以一种不依赖于eval(并且也不依赖于它不是安全问题)的方式来构造数据吗?


一个原因是字符串需要被压缩以适应App Engine的1MB实体限制,而我认为单独压缩成千上万个字符串的开销可能比将它们序列化、一起压缩并放入blob中的开销要高得多。空间节省可能也会更少。但这是一个很好的观点...我肯定在寻找避免使用eval的方法,同时又不会增加太多运行成本。 - gravitation

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