Python:我可以安全地反序列化不可信的数据吗?
pickle模块的文档一开始就有个警告:
警告: pickle模块并不保证能安全处理错误或恶意构造的数据。绝对不要对来自不可信或未经验证的来源的数据进行反序列化。
不过在文档的后面,有一部分提到限制全局变量,似乎介绍了一种通过允许的对象白名单来安全反序列化数据的方法。
这是否意味着如果我使用一个只允许某些“基本”类型的RestrictedUnpickler
,我就可以安全地反序列化不可信的数据?还是说这种方法还有其他未解决的安全问题?如果有的话,是否还有其他方法可以让反序列化变得安全(当然这可能会导致无法反序列化所有的数据流)?
这里所说的“基本类型”具体指:
bool
str
、bytes
、bytearray
int
、float
、complex
tuple
、list
、dict
、set
和frozenset
3 个回答
这个想法在邮件列表python-ideas上也讨论过,主要是为了在标准库中增加一个安全的pickle
替代方案。比如在这里提到:
为了让它更安全,我会把一个限制性的反序列化器作为默认选项(用于load/loads),如果有人想放宽限制,就必须自己重写它。为了更明确,我会让load/loads只处理内置类型。
还有在这里提到:
我一直想要一个版本的pickle.loads(),可以接受一个允许实例化的类的列表。
这样说够了吗:http://docs.python.org/3.4/library/pickle.html#restricting-globals ?
确实够了,谢谢你提到这个!我从来没有深入看过文档的模块接口部分。也许页面顶部的警告可以提到有办法缓解安全问题,并指向#restricting-globals?
是的,这主意不错 :-)
所以我不知道为什么文档没有更新,但我认为使用RestrictedUnpickler
来限制可以反序列化的类型是一个安全的解决方案。当然,库中可能存在漏洞,可能会影响系统,但OpenSSL中也可能有漏洞,会向任何请求的人显示随机的内存数据。
我甚至可以说,使用pickle来处理不可信的数据是没有安全方法的。
即使限制了一些全局变量,Python的动态特性仍然让一个有决心的黑客有机会找到回到__builtins__
映射的方法,从而接触到重要的数据。
可以看看Ned Batchelder关于绕过eval()
限制的博客,这些内容同样适用于pickle
。
记住,pickle
仍然是一种堆栈语言,你无法预见到允许任意调用后可能产生的所有对象。pickle的文档中也没有提到EXT*
操作码,这些操作码允许调用copyreg
安装的扩展;你也需要考虑在那个注册表中安装的任何东西。只要有一个途径可以将对象调用转变为getattr
的等价形式,你的防御就会崩溃。
至少要对你的数据使用加密签名,这样你可以验证数据的完整性。这样可以降低风险,但如果攻击者成功窃取了你的签名秘密(密钥),他们仍然可以给你发送一个被篡改的pickle。
我建议使用现有的无害格式,比如JSON,并添加类型注释;例如,可以将数据存储在字典中,使用一个类型键,并在加载数据时进行转换。
在这个回答中,我们将探讨pickle协议到底允许攻击者做些什么。这意味着我们只依赖协议的文档特性,而不是实现细节(有一些例外)。换句话说,我们假设pickle
模块的源代码是正确且没有漏洞的,能够让我们做文档中所说的事情,而不会多做其他的。
pickle协议允许攻击者做什么?
Pickle 允许类自定义它们的实例如何被序列化。在反序列化的过程中,我们可以:
- 调用(几乎)任何类的
__setstate__
方法(只要我们能成功反序列化该类的实例)。 - 通过
__reduce__
方法,使用任意参数调用任意可调用对象(只要我们能以某种方式访问到这个可调用对象)。 - 调用(几乎)任何反序列化对象的
append
、extend
和__setitem__
方法,这同样得益于__reduce__
。 - 访问任何
Unpickler.find_class
允许我们访问的属性。 - 自由创建以下类型的实例:
str
、bytes
、list
、tuple
、dict
、int
、float
、bool
。这并没有在文档中说明,但这些类型是协议本身内置的,不需要经过Unpickler.find_class
。
从攻击者的角度来看,最有用的特性是调用可调用对象的能力。如果他们能访问到exec
或eval
,就可以让我们执行任意代码。如果他们能访问到os.system
或subprocess.Popen
,就可以运行任意的命令。当然,我们可以通过Unpickler.find_class
来拒绝他们的访问。但我们应该如何实现我们的find_class
方法呢?哪些函数和类是安全的,哪些是危险的?
攻击者的工具箱
在这里,我将尝试解释一些攻击者可以用来做坏事的方法。给攻击者访问这些函数/类的权限意味着你处于危险之中。
- 在反序列化过程中执行任意代码:
exec
和eval
(显而易见)os.system
、os.popen
、subprocess.Popen
以及所有其他subprocess
函数types.FunctionType
,它允许从代码对象创建函数(可以通过compile
或types.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
方法阻止攻击者直接访问某些东西,并不意味着没有间接访问的方式。- 属性访问:在Python中,一切都是对象,对象有很多属性。例如,可以通过
obj.__class__
访问对象的类,通过cls.__bases__
访问类的父类,等等。
- 属性访问:在Python中,一切都是对象,对象有很多属性。例如,可以通过
索引:很多东西都存储在列表、元组和字典中,能够索引数据结构为攻击者打开了许多大门。
list.__getitem__
、dict.__getitem__
等- ... 几乎肯定还有其他
查看Ned Batchelder的Eval真的很危险,了解攻击者如何利用这些来获取几乎所有东西的访问权限。
在反序列化后执行代码:
攻击者不一定要在反序列化过程中做危险的事情——他们也可以尝试返回一个危险的对象,让你意外调用一个危险的函数。也许你在反序列化的对象上调用typing.get_type_hints
,或者你期待反序列化一个CuteBunny
,但实际上反序列化的是一个FerociousDragon
,当你试图.pet()
它时,手被咬掉。一定要确保反序列化的对象是你所期望的类型,它的属性是你所期望的类型,并且没有你不希望它有的属性。
到这里,应该很明显,能够信任的模块/类/函数并不多。当你实现find_class
方法时,永远不要写黑名单——总是写白名单,只包含你确定不会被滥用的东西。
那么问题的答案是什么?
如果你真的只允许访问bool
、str
、bytes
、bytearray
、int
、float
、complex
、tuple
、list
、dict
、set
和frozenset
,那么你很可能是安全的。但说实话——你可能应该使用JSON。
一般来说,我认为大多数类是安全的——当然有一些例外,比如subprocess.Popen
。攻击者能做的最糟糕的事情就是调用这个类——通常这不会做比返回该类的实例更危险的事情。
你真正需要小心的是允许访问函数(以及其他非类的可调用对象),以及你如何处理反序列化的对象。