Подтвердить что ты не робот

Python pickle - как он ломается?

Всем известно, что pickle не является безопасным способом хранения пользовательских данных. Он даже говорит об этом на коробке.

Я ищу примеры строк или структур данных, которые нарушают разбор парсера в текущих поддерживаемых версиях cPython >= 2.4. Есть ли вещи, которые можно мариновать, но не рассыпаться? Существуют ли проблемы с конкретными символами Unicode? Действительно большие структуры данных? Очевидно, что старый ASCII-протокол имеет некоторые проблемы, но как насчет самой текущей бинарной формы?

Мне особенно любопытно, как может работать операция pickle loads, особенно когда задана строка, созданная самим рассолом. Существуют ли какие-либо обстоятельства, при которых маринование продолжит синтаксический анализ за .?

Какие существуют краевые случаи?

Изменить: Вот несколько примеров того, что я ищу:

  • В Python 2.4 вы можете выбрать массив без ошибок, но вы не можете его разблокировать. http://bugs.python.org/issue1281383
  • Вы не можете надежно рассортировать объекты, которые наследуются от dict и вызывают __setitem__, прежде чем переменные экземпляра будут установлены с помощью __setstate__. Это может быть получение при травлении объектов Cookie. См. http://bugs.python.org/issue964868 и http://bugs.python.org/issue826897
  • Python 2.4 (и 2.5?) вернет значение рассола для бесконечности (или значения, близкие к нему, как 1e100000), но может (в зависимости от платформы) сбой при загрузке. См. http://bugs.python.org/issue880990 и http://bugs.python.org/issue445484
  • Этот последний элемент интересен тем, что в нем показан случай, когда маркер STOP фактически не останавливает синтаксический анализ - когда маркер существует как часть литерала или, в более общем плане, когда ему не предшествует новая строка.
4b9b3361

Ответ 1

Это очень упрощенный пример того, что рассол не понравился в моей структуре данных.

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)

Пикель, как известно, немного смешна с круговыми структурами, но если вы подбрасываете пользовательские хеш-функции и наборы /dicts в микс, тогда все становится довольно волосатым.

В этом конкретном примере он частично распаковывает член, а затем встречает пул. Таким образом, он частично распаковывает пул и сталкивается с установленными членами. Поэтому он создает набор и пытается добавить частично незакрашенный элемент в набор. В какой момент он умирает в пользовательской хеш-функции, потому что элемент только частично не заполнен. Я боюсь думать, что может произойти, если у вас есть "if hasattr..." в хэш-функции.

$ 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>],))

Ответ 2

Если вы заинтересованы в том, как вещи терпят неудачу с pickle (или cPickle, так как это просто немного другой импорт), вы можете использовать этот растущий список всех типов объектов в python для тестирования довольно легко.

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

В пакет dill входят функции, которые обнаруживают, как объект не может размножаться, например, поймав ошибку, которую он выдает, и возвращает ее пользователю.

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)))

Ответ 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 и сохраняет их с помощью pickle, и я могу подорвать или вставлять в это хранилище, тогда я мог бы выполнить любую команду, которую я выбрал, установив self._command напрямую.

На практике мой пример никогда не должен проходить для безопасного кода. Но учтите, что если функция sanitize безопасна, то и весь класс, кроме возможного использования рассола из ненадежных данных, нарушает это. Поэтому существуют программы, которые являются безопасными, но могут быть сделаны небезопасными из-за неправильного использования рассола.

Опасность заключается в том, что ваш код, способный использовать марихуану, может быть подорван по тому же принципу, но в невинно выглядящем коде, где уязвимость гораздо менее очевидна. Лучше всего всегда избегать использования маринования для загрузки ненадежных данных.