Python pickle - 它是怎么坏的?

13 投票
3 回答
3990 浏览
提问于 2025-04-16 06:47

大家都知道,使用pickle来存储用户数据并不安全。这在说明书上也写得很清楚。

我想找一些例子,看看在当前支持的版本中,哪些字符串或数据结构会让pickle解析失败,版本是cPython >= 2.4。有没有一些可以被pickle化但不能被还原的数据?特定的unicode字符会有问题吗?特别大的数据结构呢?显然,旧的ASCII协议有一些问题,但最新的二进制格式又怎么样呢?

我特别想知道pickle的loads操作可能在哪些情况下会失败,尤其是当输入的是pickle自己生成的字符串时。有没有可能在某些情况下,pickle会继续解析到.之后?

有哪些边缘案例呢?

补充:以下是我想要的那种例子的几个例子:

3 个回答

0

可以对类的实例进行序列化,也就是我们常说的“腌制”。如果我知道你的应用程序使用了哪些类,我就可以对它们进行一些恶意操作。举个简单的例子:

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函数是安全的,那么整个类也是安全的,前提是没有使用来自不可信数据的腌制功能来破坏这一点。因此,确实存在一些程序是安全的,但因为不当使用腌制功能而变得不安全。

危险在于,你的使用腌制功能的代码可能会在看似无害的代码中被攻击,而这种漏洞并不明显。最好的做法是,始终避免使用腌制功能来加载不可信的数据。

2

如果你对使用 pickle(或者 cPickle,它只是一个稍微不同的导入方式)时出现的问题感兴趣,你可以利用这个不断增加的 Python 中各种对象类型的列表来进行简单的测试。

https://github.com/uqfoundation/dill/blob/master/dill/_objects.py

这个叫 dill 的包里有一些功能,可以帮助你发现一个对象在使用 pickle 时为什么会失败,比如通过捕捉它抛出的错误并把错误信息返回给用户。

dill.dill 里有这些功能,你也可以为 picklecPickle 创建类似的功能,只需要简单地复制粘贴,并使用 import pickleimport 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)))
6

这是一个非常简单的例子,说明了为什么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>],))

撰写回答