Как безопасно вернуть объекты из песочницы Python на уровне ОС?

Мне нужно запускать ненадежные скрипты Python. После долгих исследований кажется, что песочница только для Python небезопасна, по крайней мере, с CPython (который мне нужно использовать).

Поэтому мы также планируем использовать песочницу на уровне ОС (SELinux, AppArmor и т. Д.).

У меня вопрос: как безопасно общаться с песочницей? Код в песочнице должен будет возвращать типы Python, такие как int и str, а также массивы Numpy. В будущем может появиться больше типов.

Очевидный способ - использовать pickle, но кажется возможным, что какой-то вредоносный код в песочнице может захватить выходной канал (мы думали об использовании 0MQ) и отправить обратно что-то, что может привести к выполнению произвольного кода, если его не выбрать за пределами песочница.

Существуют ли более безопасные альтернативы сериализации для pickle, которые не влияют на производительность JSON и тому подобное?

Мы используем Python 3.3.


person capitalistcuttle    schedule 11.11.2013    source источник
comment
В быстром тесте с простым dict со 100К списками из 100 элементов pickle.dumps занял 35,6 мс и json.dumps 71,6 мс. Вас беспокоят накладные расходы на производительность? Или есть что-то другое в ваших данных? (По результатам быстрого теста создание 100 тыс. Копий того же списка pickle значительно ускоряет, но json совсем нет; это то, с чем вы имеете дело?)   -  person abarnert    schedule 11.11.2013
comment
В любом случае, если вы хотите что-то, что работает только с ограниченным белым списком типов, начиная с безопасного подмножества YAML и добавляя явные обработчики для вашего белого списка (вместо использования расширяемого YAML и попытки его ограничить), должно быть точно так же безопасно, как вам нужно. . Но он может быть не быстрее, чем пользовательский расширенный JSON, который вы, вероятно, рассматривали; это может быть даже медленнее. Просто YAML был разработан для расширения таким образом, а JSON - нет (что означает, например, что общие расширяемые анализаторы YAML смогут читать ваши данные для целей отладки), поэтому, возможно, стоит попробовать.   -  person abarnert    schedule 11.11.2013
comment
Наконец, уверены ли вы, что ограничения типов достаточно? Скорее всего, любой, кто сможет заполучить пайп, сможет создать гигантские целые числа или что-то еще, чтобы DoS-атаковать ваш код; если строки представляют собой какие-либо внешние ресурсы, такие как URL-адреса или пути, они могут передавать любые строки, какие захотят; и т.д. Это нормально?   -  person abarnert    schedule 11.11.2013
comment
Dicts в порядке, но массивы numpy и DataFrames pandas (на основе массивов numpy), похоже, не так хороши. Для DataFrame размером 1000x100 дамп в JSON занимает 15 мс, загрузка - 65 мс. Выгрузка через рассол занимает 250 микросекунд (не милли), а загрузка занимает 150 микросекунд.   -  person capitalistcuttle    schedule 11.11.2013
comment
И спасибо. Я посмотрю на YAML. DoS тоже есть о чем подумать.   -  person capitalistcuttle    schedule 11.11.2013


Ответы (1)


Похоже, ваша единственная реальная проблема с JSON - это способ кодирования массивов NumPy (и таблиц Pandas). JSON не идеален для вашего варианта использования - не потому, что он медленно обрабатывает данные NumPy, а потому, что это текстовый формат, и у вас есть много данных, которые легче кодировать в нетекстовом формате.

Итак, я покажу вам способ решения всех ваших проблем с JSON ниже ... но я бы предложил использовать другой формат.

Два основных двоичных формата JSON: BJSON и BSON, стремятся предоставить большинство преимуществ JSON (простой, безопасный, динамический / бессхемный, проходимый и т. д.), а также сделать возможным прямое встраивание двоичных данных. (Тот факт, что они также являются двоичными, а не текстовыми форматами, в данном случае для вас не особо важен.) Я считаю, что то же самое верно и для Улыбнись, но никогда этим не пользовался.

Это означает, что точно так же, как JSON позволяет легко подключать все, что вы можете свести к строкам, числам с плавающей запятой, спискам и диктовкам, BJSON и BSON позволяют легко подключать все, что вы можете свести к строкам, числам с плавающей запятой, спискам, диктовкам. , и байтовые строки. Итак, когда я показываю, как кодировать / декодировать NumPy в строки, то же самое работает для байтовых строк, но без всех дополнительных шагов в конце.

Недостатки BJSON и BSON в том, что они не читаются человеком и не имеют такой широкой поддержки.


Я понятия не имею, как вы в настоящее время кодируете свои массивы, но, судя по таймингу, я подозреваю, что вы используете метод tolist или что-то подобное. Это определенно будет медленным и большим. И он даже потеряет информацию, если вы где-то храните что-либо, кроме f8 значений (потому что JSON понимает только числа типа IEEE double). Решение заключается в кодировании в строку.

NumPy имеет текстовый формат, который будет быстрее и без потерь, но все же, вероятно, медленнее и больше, чем вы хотите.

Он также имеет двоичный формат, что здорово ... но не имеет достаточной информации для восстановления исходного массива.

Итак, давайте посмотрим, что использует pickle, что вы можете увидеть, вызвав метод __reduce__ для любого объекта: в основном, это тип, форма, dtype, некоторые флаги, которые сообщают NumPy, как интерпретировать необработанные данные, а затем двоичный -форматировать необработанные данные. Фактически вы можете сами закодировать __reduce__ данные - на самом деле, это может стоить того. Но давайте сделаем что-нибудь попроще для пояснения, понимая, что это будет работать только на ndarray и не будет работать на машинах с другим порядком байтов (или в более редких случаях, таких как целые числа знака или числа с плавающей запятой, не относящиеся к IEEE).

def numpy_default(obj):
    if isinstance(obj, np.ndarray):
        return {'_npdata': obj.tostring(), 
                '_npdtype': obj.dtype.name,
                '_npshape': obj.shape}
    else:
        return json.dumps(obj)

def dumps(obj):
    return json.dumps(obj, default=numpy_default)

def numpy_hook(obj):
    try:
        data = obj['_npdata']
    except AttributeError:
        return obj
    return np.fromstring(data, obj['_npdtype']).reshape(obj['_npshape'])

def loads(obj):
    return json.loads(obj, object_hook=numpy_hook)

Единственная проблема в том, что np.tostring дает вам 'bytes' объектов, с которыми json Python 3 не знает, как справиться.

На этом вы можете остановиться, если используете что-то вроде BJSON или BSON. Но с JSON вам нужны строки.

Вы можете легко это исправить, хотя и хакерски, «декодируя» байты с помощью любой кодировки, которая отображает каждый однобайтовый символ, например Latin-1: измените obj.tostring() на obj.tostring().decode('latin-1') и data = obj['_npdata'] на data = obj['_npdata'].encode('latin-1'). Это тратит немного места на кодировку UTF-8 поддельных строк Latin-1, но это не слишком плохо.

К сожалению, Python будет кодировать каждый символ, отличный от ASCII, с помощью escape-последовательности Unicode. Вы можете отключить это, установив ensure_ascii=False для дампа и strict=False для загрузки, но он по-прежнему будет кодировать управляющие символы, в основном в 6-байтовые последовательности. Это удваивает размер случайных данных, и это может сделать намного хуже - например, массив с нулевыми значениями будет в 6 раз больше!

Раньше существовал трюк, чтобы обойти эту проблему, но в 3.3 он не работает. Лучшее, что вы можете сделать, - это форкнуть или исправить пакет json, чтобы он позволял передавать управляющие символы при задании ensure_ascii=False, что можно сделать следующим образом:

json.encoder.ESCAPE = re.compile(r'"')

Это довольно хитроумно, но работает.


В любом случае, надеюсь, этого достаточно, чтобы вы начали.

person abarnert    schedule 11.11.2013
comment
Большое спасибо за это. Я собираюсь поиграть с этим еще немного (и просто подождать немного, чтобы посмотреть, вмешаются ли другие, прежде чем принять), но вы очень помогли. Я вошел в маринование Pandas, и, похоже, он в основном вызывает reduce для своих массивов numpy, которые вы показали мне, как очень эффективно сериализовать с помощью BSON (я действительно использовал JSON и списки ранее). И в Pandas есть некоторые внутренние элементы, которые разбивают DataFrame на однородные массивы numpy, так что я должен быть на своем пути! - person capitalistcuttle; 11.11.2013
comment
Чем сложнее типы, тем больше вам захочется опираться на __reduce__ вместо того, чтобы писать все с нуля. Например, другие значения, которые дает ndarray.__reduce__, позволяют различать данные с прямым и обратным порядком байтов или отличать ndarray от matrix и т. Д. Вам просто нужно написать ручной код для сопоставления этих значений со строками, а затем обратно к типам / конструкторы для типов (желательно коротких и статических белых списков), с которыми вы хотите работать, чтобы никто не смог обманом заставить вас десериализовать вещи, которые вам не нужны. - person abarnert; 11.11.2013
comment
Еще раз спасибо. Если я построю новый массив numpy с формой и dtype старого массива и попробую newarray.__setstate__(oldarray.__reduce__()), я получу TypeError: must be sequence of length 4, not 3. Как перейти от сокращения к состоянию набора? (Я тоже задал отдельный вопрос, касающийся Pandas.) - person capitalistcuttle; 12.11.2013
comment
@ user2461398: __setstate__ работает со значениями из __getstate__, а не __reduce__. Документы pickle объясняют, что означает форма кортежа __reduce__. В основном вы делаете obj = r[0](*r[1]); obj.__setstate__(r[2]). Однако, если вы посмотрите, каковы значения, вы можете сделать что-то более простое и, возможно, более безопасное. Во-первых, obj = r[1][0](*r[1][1:]) пропускает скрытый _reconstruct вызов. Но также вы можете просмотреть информацию о состоянии) в r[2] - это версия рассола, форма, dtype, флаг порядка строк и необработанные данные. - person abarnert; 12.11.2013