Python pickle - 它是怎么坏的?
大家都知道,使用pickle来存储用户数据并不安全。这在说明书上也写得很清楚。
我想找一些例子,看看在当前支持的版本中,哪些字符串或数据结构会让pickle解析失败,版本是cPython >= 2.4
。有没有一些可以被pickle化但不能被还原的数据?特定的unicode字符会有问题吗?特别大的数据结构呢?显然,旧的ASCII协议有一些问题,但最新的二进制格式又怎么样呢?
我特别想知道pickle的loads
操作可能在哪些情况下会失败,尤其是当输入的是pickle自己生成的字符串时。有没有可能在某些情况下,pickle会继续解析到.
之后?
有哪些边缘案例呢?
补充:以下是我想要的那种例子的几个例子:
- 在Python 2.4中,你可以成功pickle一个数组,但无法将其还原。http://bugs.python.org/issue1281383
- 你不能可靠地pickle那些继承自字典的对象,并且在实例变量通过
__setstate__
设置之前调用__setitem__
。这在pickle Cookie对象时可能会出问题。请参见http://bugs.python.org/issue964868和http://bugs.python.org/issue826897 - Python 2.4(还有2.5?)会返回一个pickle值表示无穷大(或者接近无穷大的值,比如1e100000),但在加载时可能会失败(这取决于平台)。请参见http://bugs.python.org/issue880990和http://bugs.python.org/issue445484
- 最后这个例子很有趣,因为它揭示了一个情况:
STOP
标记并没有真正停止解析——当这个标记作为字面量的一部分存在时,或者更一般地说,当它前面没有换行符时。
3 个回答
可以对类的实例进行序列化,也就是我们常说的“腌制”。如果我知道你的应用程序使用了哪些类,我就可以对它们进行一些恶意操作。举个简单的例子:
import subprocess
class Command(object):
def __init__(self, command):
self._command = self._sanitize(command)
@staticmethod
def _sanitize(command):
return filter(lambda c: c in string.letters, command)
def run(self):
subprocess.call('/usr/lib/myprog/%s' % self._command, shell=True)
假设你的程序创建了Command
实例,并使用腌制功能保存它们。如果我能够对这个存储进行恶意操作或者注入代码,那么我就可以直接设置self._command
,从而执行我想要的任何命令。
实际上,我的这个例子本身就不应该被认为是安全的代码。不过要注意,如果sanitize
函数是安全的,那么整个类也是安全的,前提是没有使用来自不可信数据的腌制功能来破坏这一点。因此,确实存在一些程序是安全的,但因为不当使用腌制功能而变得不安全。
危险在于,你的使用腌制功能的代码可能会在看似无害的代码中被攻击,而这种漏洞并不明显。最好的做法是,始终避免使用腌制功能来加载不可信的数据。
如果你对使用 pickle
(或者 cPickle
,它只是一个稍微不同的导入方式)时出现的问题感兴趣,你可以利用这个不断增加的 Python 中各种对象类型的列表来进行简单的测试。
https://github.com/uqfoundation/dill/blob/master/dill/_objects.py
这个叫 dill
的包里有一些功能,可以帮助你发现一个对象在使用 pickle 时为什么会失败,比如通过捕捉它抛出的错误并把错误信息返回给用户。
dill.dill
里有这些功能,你也可以为 pickle
或 cPickle
创建类似的功能,只需要简单地复制粘贴,并使用 import pickle
或 import cPickle as pickle
(或者 import dill as pickle
):
def copy(obj, *args, **kwds):
"""use pickling to 'copy' an object"""
return loads(dumps(obj, *args, **kwds))
# quick sanity checking
def pickles(obj,exact=False,safe=False,**kwds):
"""quick check if object pickles with dill"""
if safe: exceptions = (Exception,) # RuntimeError, ValueError
else:
exceptions = (TypeError, AssertionError, PicklingError, UnpicklingError)
try:
pik = copy(obj, **kwds)
try:
result = bool(pik.all() == obj.all())
except AttributeError:
result = pik == obj
if result: return True
if not exact:
return type(pik) == type(obj)
return False
except exceptions:
return False
并且在 dill.detect
中包含这些功能:
def baditems(obj, exact=False, safe=False): #XXX: obj=globals() ?
"""get items in object that fail to pickle"""
if not hasattr(obj,'__iter__'): # is not iterable
return [j for j in (badobjects(obj,0,exact,safe),) if j is not None]
obj = obj.values() if getattr(obj,'values',None) else obj
_obj = [] # can't use a set, as items may be unhashable
[_obj.append(badobjects(i,0,exact,safe)) for i in obj if i not in _obj]
return [j for j in _obj if j is not None]
def badobjects(obj, depth=0, exact=False, safe=False):
"""get objects that fail to pickle"""
if not depth:
if pickles(obj,exact,safe): return None
return obj
return dict(((attr, badobjects(getattr(obj,attr),depth-1,exact,safe)) \
for attr in dir(obj) if not pickles(getattr(obj,attr),exact,safe)))
def badtypes(obj, depth=0, exact=False, safe=False):
"""get types for objects that fail to pickle"""
if not depth:
if pickles(obj,exact,safe): return None
return type(obj)
return dict(((attr, badtypes(getattr(obj,attr),depth-1,exact,safe)) \
for attr in dir(obj) if not pickles(getattr(obj,attr),exact,safe)))
还有这个最后的功能,你可以用它来测试 dill._objects
中的对象。
def errors(obj, depth=0, exact=False, safe=False):
"""get errors for objects that fail to pickle"""
if not depth:
try:
pik = copy(obj)
if exact:
assert pik == obj, \
"Unpickling produces %s instead of %s" % (pik,obj)
assert type(pik) == type(obj), \
"Unpickling produces %s instead of %s" % (type(pik),type(obj))
return None
except Exception:
import sys
return sys.exc_info()[1]
return dict(((attr, errors(getattr(obj,attr),depth-1,exact,safe)) \
for attr in dir(obj) if not pickles(getattr(obj,attr),exact,safe)))
这是一个非常简单的例子,说明了为什么pickle对我的数据结构不太满意。
import cPickle as pickle
class Member(object):
def __init__(self, key):
self.key = key
self.pool = None
def __hash__(self):
return self.key
class Pool(object):
def __init__(self):
self.members = set()
def add_member(self, member):
self.members.add(member)
member.pool = self
member = Member(1)
pool = Pool()
pool.add_member(member)
with open("test.pkl", "w") as f:
pickle.dump(member, f, pickle.HIGHEST_PROTOCOL)
with open("test.pkl", "r") as f:
x = pickle.load(f)
大家知道,pickle在处理循环结构时有点麻烦,但如果你再加上自定义的哈希函数和集合/字典,那事情就会变得相当复杂。
在这个具体的例子中,它先部分地解码了一个成员,然后遇到了一个池子。接着,它又部分解码了这个池子,遇到了成员集合。于是它创建了这个集合,并试图把那个部分解码的成员添加到集合里。结果在自定义的哈希函数那里出错了,因为这个成员只被部分解码。如果哈希函数里有“如果有这个属性...”的判断,那我真不敢想象会发生什么。
$ python --version
Python 2.6.5
$ python test.py
Traceback (most recent call last):
File "test.py", line 25, in <module>
x = pickle.load(f)
File "test.py", line 8, in __hash__
return self.key
AttributeError: ("'Member' object has no attribute 'key'", <type 'set'>, ([<__main__.Member object at 0xb76cdaac>],))