Главная
Блог разработчиков phpBB
 
+ 17 предустановленных модов
+ SEO-оптимизация форума
+ авторизация через соц. сети
+ защита от спама

И вновь про угроза eval()

Anna | 15.06.2014 | нет комментариев

Сколько было сломано копий при обсуждении вопроса «Допустимо ли сделать eval неопасным?» — нереально сосчитать. Неизменно находится кто-то, кто заявляет, что нашёл метод оградиться от всех допустимых последствий выполнения этой функции.
Когда мне потребовалось обнаружить развёрнутый результат на данный вопрос, я наткнулся на один пост. Меня славно поразила глубина изыскания, так что я решил, что это стоит перевести.

Коротко о задаче

В Python есть встроенная функция eval(), которая исполняет строку с кодом и возвращает итог выполнения:

assert eval("2   3 * len('hello')") == 17

Это дюже сильная, но в то же время и дюже опасная инструкция, исключительно если строки, которые вы передаёте в eval, получены не из доверенного источника. Что будет, если строкой, которую мы решим скормить eval‘у, окажется os.system('rm -rf /')? Интерпретатор Добросовестно запустит процесс удаления всех данных с компьютера, и отлично ещё, если он будет выполняться от имени наименее привилегированного пользователя (в последующих примерах я буду применять clear (cls, если вы используете Windows) взамен rm -rf /, Дабы никто из читателей нечаянно не выстрелил себе в ногу).

Какие есть решения?

Некоторые утверждают, что допустимо сделать eval безвредным, если запускать его без доступа к символам из globals. В качестве второго (опционального) довода eval() принимает словарь, тот, что будет использован взамен глобального пространства имён (все классы, способы, переменные и пр., объявленные на «верхнем» ярусе, доступные из всякий точки кода) кодом, тот, что будет исполнен eval‘ом. Если eval вызывается без этого довода, он использует нынешнее глобальное пространство имён, в которое мог быть импортирован модуль os. Если же передать пустой словарь, глобальное пространство имён для eval‘а будет пустым. Вот такой код теснее не сумеет выполниться и возбудит исключение NameError: name 'os' is not defined:

eval("os.system('clear')", {})

Впрочем мы всё ещё можем импортировать модули и обращаться к ним, применяя встроенную функцию__import__. Так, код ниже отработает без ошибок:

eval("__import__('os').system('clear')", {})

Дальнейшей попыткой традиционно становится решение запретить доступ к __builtins__ изнутри eval‘a, так как имена, сходственные __import__, доступны нам потому, что они находятся в всеобщей переменной__builtins__. Если мы очевидно передадим взамен неё пустой словарь, код ниже теснее не сумеет быть исполнен:

eval("__import__('os').system('clear')", {'__builtins__':{}}) # NameError: name '__import__' is not defined

Ну а сейчас-то мы в безопасности?


Некоторые говорят, что «да» и делают ошибку. Для примера, вот данный маленький кусок кода вызоветsegfault, если вы запустите его в CPython:

s = """
(lambda fc=(
    lambda n: [
        c for c in 
            ().__class__.__bases__[0].__subclasses__() 
            if c.__name__ == n
        ][0]
    ):
    fc("function")(
        fc("code")(
            0,0,0,0,"KABOOM",(),(),(),"","",0,""
        ),{}
    )()
)()
"""
eval(s, {'__builtins__':{}})

Выходит, давайте разберёмся, что же тут происходит. Начнём с этого:

().__class__.__bases__[0]

Как многие могли додуматься, это легко один из методов обратиться к object. Мы не можем легко написатьobject, так как __builtins__ пусты, но мы можем сделать пустой кортеж (тьюпл), первым базовым классом которого является object и, пройдясь по его свойствам, получить доступ к классу object.
Сейчас мы получаем список всех классов, которые наследуют object либо, иными словами, список всех классов, объявленных в программе на данный момент:

().__class__.__bases__[0].__subclasses__() 

Если заменить для удобочитаемости это выражение на ALL_CLASSES, несложно будет подметить, что выражение ниже находит класс по его имени:

[c for c in ALL_CLASSES if c.__name__ == n][0]

Дальше в коде нам нужно будет двукратно искать класс, так что сотворим функцию:

lambda n: [c for c in ALL_CLASSES if c.__name__ == n][0]

Дабы вызвать функцию, нужно как-то её назвать, но, так как мы будем исполнять данный код внутри eval‘a, мы не можем ни объявить функцию (применяя def), ни применять оператор присвоения, Дабы привязать нашу лямбду к какой-нибудь переменной.
Впрочем, есть и 3-й вариант: параметры по умолчанию. При объявлении лямбды, как и при объявлении всякий обыкновенной функции, мы можем задать параметры по умолчанию, так что если мы разместим каждый код внутри ещё одной лямбды, и зададим ей нашу, как параметр по умолчанию, — мы добьёмся желаемого:

(lambda fc=(
    lambda n: [
        c for c in ALL_CLASSES if c.__name__ == n
        ][0]
    ):
    # сейчас мы можем обращаться к нашей лямбде через fc
)()

Выходит, мы имеем функцию, которая может искать классы, и можем обращаться к ней по имени. Что дальше? Мы сделаем объект класса code (внутренний класс, его экземпляром, скажем, является качество func_codeобъекта функции):

fc("code")(0,0,0,0,"KABOOM",(),(),(),"","",0,"")

Из всех инициализующих параметров нас волнует только «KABOOM». Это и есть последовательность байт-кодов, которую будет применять наш объект, и, как вы теснее могли додуматься, эта последовательность не является «отличной». На самом деле всякого байт-кода из неё хватило бы, так как всё это — бинарные операторы, которые будут вызваны при пустом стеке, что приведёт к segfault‘у CPython. “KABOOM“ легко выглядит комичнее, спасибо lvh за данный пример.

Выходит, у нас есть объект класса code, но напрямую исполнить его мы не можем. Тогда сделаем функцию, кодом которой и будет наш объект:

fc("function")(CODE_OBJECT, {})

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

(lambda fc=(lambda n: [c for c in ().__class__.__bases__[0].__subclasses__() if c.__name__ == n][0]):
    fc("function")(fc("code")(0,0,0,0,"KABOOM",(),(),(),"","",0,""),{})()
)()

Завершение

Выходит, верю сейчас ни у кого не осталось сомнений в том, что eval НЕ НЕОПАСЕН, даже если убрать доступ к глобальным и встроенным переменным.

В примере выше мы применяли список всех подклассов класса object, Дабы сделать объекты классов code иfunction. Верно таким же образом дозволено получить (и инстанцировать) всякий класс, присутствующий в программе на момент вызова eval().
Вот ещё один пример того, что дозволено сделать:

s = """
[
    c for c in 
    ().__class__.__bases__[0].__subclasses__() 
    if c.__name__ == "Quitter"
][0](0)()
"""
eval(s, {'__builtins__':{}})

Модуль lib/site.py содержит класс Quitter, тот, что вызывается интерпретатором, когда вы набираете quit().
Код выше находит данный класс, инстанциирует его и вызывает, чем завершает работу интерпретатора.

Теперь мы запускали eval в пустом окружении, исходя из того, что указанный в статье код — это каждый код нашей программы.
В случае применения eval‘а в настоящем приложении преступник может получить доступ ко каждому классам, которые вы используете, так что его вероятности не будут ограничены фактически ничем.

Задача всех сходственных попыток сделать eval безвредным в том, что они все основаны на идее «чёрных списков», идее о том, что нужно убрать доступ ко каждому вещам, которые, как нам кажется, могут быть опасны при применении в eval‘е. С такой стратегией фактически нет шансов на победу, чай если окажется незапрещённым хоть что-то, система будет уязвима.

Когда я проводил изыскание этой темы, я наткнулся на защищенный режим выполнения eval‘а в Python, тот, что является ещё одной попыткой побороть эту задачу:

>>> eval("(lambda:0).func_code", {'__builtins__':{}})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
RuntimeError: function attributes not accessible in restricted mode

Лаконично, он работает дальнейшим образом: если __builtins__ внутри eval отличаются от «официальных» — eval переходит в защищенный режим, в котором закрыт доступ к некоторым опасным свойствам, таким какfunc_code у функций. Больше подробное изложение этого режима дозволено обнаружить здесь, но, как мы теснее видели выше, он тоже не является «серебряной пулей».

И всё-таки, дозволено ли сделать eval безвредным? Трудно сказать. Как мне кажется, преступнику не удастся навредить без доступа к объектам с двумя нижними подчёркиваниями, обрамляющими имя, так чтодопустимо, если исключить из обработки все строки с двумя нижними подчёркиваниями, то мы будем в безопасности. Допустимо…

P.S.

В треде на Reddit я нашёл короткий сниппет, разрешающий нам в eval получить «подлинные» __builtins__:

[
    c for c in ().__class__.__base__.__subclasses__() 
    if c.__name__ == 'catch_warnings'
][0]()._module.__builtins__

 

Традиционное P.P.S. для програ: умоляю обо всех ошибках, неточностях и опечатках писать в личку :)

Источник: programmingmaster.ru

Оставить комментарий
Форум phpBB, русская поддержка форума phpBB
Рейтинг@Mail.ru 2008 - 2017 © BB3x.ru - русская поддержка форума phpBB