Сообщить об ошибке.

Распаковка pickle данных из ненадежных источников

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

Просто представьте, что этот поток данных делает при загрузке:

>>> import pickle
>>> pickle.loads(b"cos\nsystem\n(S'echo hello world'\ntR.")
# hello world
# 0

В этом примере unpickler импортирует функцию os.system() и затем выполняет команду echo hello world в системной оболочке bash. Этот пример безобиден, но нетрудно представить что может сделать вызов команды rm -rf ~/*.

По этой причине необходимо контролировать те данные, которые приходят из ненадежных источников, настраивая метод Unpickler.find_class(). В отличие от его названия, метод Unpickler.find_class() вызывается всякий раз, когда пользовательский класс или функция запрашивает встроенные классы, функции и модули. Таким образом, можно либо полностью запретить их выполнение, либо ограничить их безопасным подмножеством.

Вот пример разборщика, который позволяет загружать только несколько безопасных классов:

import builtins
import io
import pickle

safe_builtins = {
    'range',
    'complex',
    'set',
    'frozenset',
    'slice',
}

class RestrictedUnpickler(pickle.Unpickler):

    def find_class(self, module, name):
        # Разрешить только безопасные встроенные классы.
        if module == "builtins" and name in safe_builtins:
            return getattr(builtins, name)
        # Запретить все остальное..
        raise pickle.UnpicklingError("global '%s.%s' is forbidden" %
                                     (module, name))

def restricted_loads(s):
    """Вспомогательная функция аналогична 'pickle.loads()'."""
    return RestrictedUnpickler(io.BytesIO(s)).load()

Пример использования:

>>> restricted_loads(pickle.dumps([1, 2, range(15)]))
# [1, 2, range(0, 15)]
>>> restricted_loads(b"cos\nsystem\n(S'echo hello world'\ntR.")
# Traceback (most recent call last):
#   ...
# pickle.UnpicklingError: global 'os.system' is forbidden
>>> restricted_loads(b'cbuiltins\neval\n'
...                  b'(S\'getattr(__import__("os"), "system")'
...                  b'("echo hello world")\'\ntR.')
# Traceback (most recent call last):
#   ...
# pickle.UnpicklingError: global 'builtins.eval' is forbidden

Как показывает опыт, необходимо ВСЕГДА быть аккуратным с данными, приходящими из ненадежных источников.