Python:我可以安全地反序列化不可信的数据吗?

16 投票
3 回答
11403 浏览
提问于 2025-04-18 17:35

pickle模块的文档一开始就有个警告:

警告: pickle模块并不保证能安全处理错误或恶意构造的数据。绝对不要对来自不可信或未经验证的来源的数据进行反序列化。

不过在文档的后面,有一部分提到限制全局变量,似乎介绍了一种通过允许的对象白名单来安全反序列化数据的方法。

这是否意味着如果我使用一个只允许某些“基本”类型的RestrictedUnpickler,我就可以安全地反序列化不可信的数据?还是说这种方法还有其他未解决的安全问题?如果有的话,是否还有其他方法可以让反序列化变得安全(当然这可能会导致无法反序列化所有的数据流)?

这里所说的“基本类型”具体指:

  • bool
  • strbytesbytearray
  • intfloatcomplex
  • tuplelistdictsetfrozenset

3 个回答

2

这个想法在邮件列表python-ideas上也讨论过,主要是为了在标准库中增加一个安全的pickle替代方案。比如在这里提到:

为了让它更安全,我会把一个限制性的反序列化器作为默认选项(用于load/loads),如果有人想放宽限制,就必须自己重写它。为了更明确,我会让load/loads只处理内置类型。

还有在这里提到:

我一直想要一个版本的pickle.loads(),可以接受一个允许实例化的类的列表。

这样说够了吗:http://docs.python.org/3.4/library/pickle.html#restricting-globals ?

确实够了,谢谢你提到这个!我从来没有深入看过文档的模块接口部分。也许页面顶部的警告可以提到有办法缓解安全问题,并指向#restricting-globals?

是的,这主意不错 :-)

所以我不知道为什么文档没有更新,但我认为使用RestrictedUnpickler来限制可以反序列化的类型是一个安全的解决方案。当然,库中可能存在漏洞,可能会影响系统,但OpenSSL中也可能有漏洞,会向任何请求的人显示随机的内存数据。

8

我甚至可以说,使用pickle来处理不可信的数据是没有安全方法的。

即使限制了一些全局变量,Python的动态特性仍然让一个有决心的黑客有机会找到回到__builtins__映射的方法,从而接触到重要的数据。

可以看看Ned Batchelder关于绕过eval()限制的博客,这些内容同样适用于pickle

记住,pickle仍然是一种堆栈语言,你无法预见到允许任意调用后可能产生的所有对象。pickle的文档中也没有提到EXT*操作码,这些操作码允许调用copyreg安装的扩展;你也需要考虑在那个注册表中安装的任何东西。只要有一个途径可以将对象调用转变为getattr的等价形式,你的防御就会崩溃。

至少要对你的数据使用加密签名,这样你可以验证数据的完整性。这样可以降低风险,但如果攻击者成功窃取了你的签名秘密(密钥),他们仍然可以给你发送一个被篡改的pickle。

我建议使用现有的无害格式,比如JSON,并添加类型注释;例如,可以将数据存储在字典中,使用一个类型键,并在加载数据时进行转换。

22

在这个回答中,我们将探讨pickle协议到底允许攻击者做些什么。这意味着我们只依赖协议的文档特性,而不是实现细节(有一些例外)。换句话说,我们假设pickle模块的源代码是正确且没有漏洞的,能够让我们做文档中所说的事情,而不会多做其他的。

pickle协议允许攻击者做什么?

Pickle 允许类自定义它们的实例如何被序列化。在反序列化的过程中,我们可以:

  • 调用(几乎)任何类的__setstate__方法(只要我们能成功反序列化该类的实例)。
  • 通过__reduce__方法,使用任意参数调用任意可调用对象(只要我们能以某种方式访问到这个可调用对象)。
  • 调用(几乎)任何反序列化对象的appendextend__setitem__方法,这同样得益于__reduce__
  • 访问任何Unpickler.find_class允许我们访问的属性。
  • 自由创建以下类型的实例:strbyteslisttupledictintfloatbool。这并没有在文档中说明,但这些类型是协议本身内置的,不需要经过Unpickler.find_class

从攻击者的角度来看,最有用的特性是调用可调用对象的能力。如果他们能访问到execeval,就可以让我们执行任意代码。如果他们能访问到os.systemsubprocess.Popen,就可以运行任意的命令。当然,我们可以通过Unpickler.find_class来拒绝他们的访问。但我们应该如何实现我们的find_class方法呢?哪些函数和类是安全的,哪些是危险的?

攻击者的工具箱

在这里,我将尝试解释一些攻击者可以用来做坏事的方法。给攻击者访问这些函数/类的权限意味着你处于危险之中。

  • 在反序列化过程中执行任意代码:
    • execeval(显而易见)
    • os.systemos.popensubprocess.Popen以及所有其他subprocess函数
    • types.FunctionType,它允许从代码对象创建函数(可以通过compiletypes.CodeType创建)
    • typing.get_type_hints。没错,你没听错。你问怎么做到的?好吧,typing.get_type_hints会评估前向引用。所以你只需要一个带有__annotations__的对象,比如{'x': 'os.system("rm -rf /")'}get_type_hints就会为你运行这段代码。
    • functools.singledispatch。我看到你在摇头不信,但这是真的。单分派函数有一个register方法,它内部调用typing.get_type_hints
    • ... 可能还有其他一些
  • 不通过Unpickler.find_class访问事物:

    仅仅因为我们的find_class方法阻止攻击者直接访问某些东西,并不意味着没有间接访问的方式。

    查看Ned Batchelder的Eval真的很危险,了解攻击者如何利用这些来获取几乎所有东西的访问权限。

  • 在反序列化执行代码:

    攻击者不一定要在反序列化过程中做危险的事情——他们也可以尝试返回一个危险的对象,让意外调用一个危险的函数。也许你在反序列化的对象上调用typing.get_type_hints,或者你期待反序列化一个CuteBunny,但实际上反序列化的是一个FerociousDragon,当你试图.pet()它时,手被咬掉。一定要确保反序列化的对象是你所期望的类型,它的属性是你所期望的类型,并且没有你不希望它有的属性。

到这里,应该很明显,能够信任的模块/类/函数并不多。当你实现find_class方法时,永远不要写黑名单——总是写白名单,只包含你确定不会被滥用的东西。

那么问题的答案是什么?

如果你真的只允许访问boolstrbytesbytearrayintfloatcomplextuplelistdictsetfrozenset,那么你很可能是安全的。但说实话——你可能应该使用JSON。

一般来说,我认为大多数是安全的——当然有一些例外,比如subprocess.Popen。攻击者能做的最糟糕的事情就是调用这个类——通常这不会做比返回该类的实例更危险的事情。

你真正需要小心的是允许访问函数(以及其他非类的可调用对象),以及你如何处理反序列化的对象。

撰写回答