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
фактически не останавливает синтаксический анализ - когда маркер существует как часть литерала или, в более общем плане, когда ему не предшествует новая строка.
Ответы
Ответ 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
безопасна, то и весь класс, кроме возможного использования рассола из ненадежных данных, нарушает это. Поэтому существуют программы, которые являются безопасными, но могут быть сделаны небезопасными из-за неправильного использования рассола.
Опасность заключается в том, что ваш код, способный использовать марихуану, может быть подорван по тому же принципу, но в невинно выглядящем коде, где уязвимость гораздо менее очевидна. Лучше всего всегда избегать использования маринования для загрузки ненадежных данных.