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

Python eval: все еще опасно, если я отключу встроенные функции и доступ к атрибутам?

Мы все знаем, что eval является опасным, даже если вы скрываете опасные функции, потому что вы можете использовать функции интроспекции Python, чтобы выкапывать вещи и повторно извлекать их. Например, даже если вы удалите __builtins__, вы можете получить их с помощью

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

Однако каждый пример, который я видел, использует доступ к атрибутам. Что делать, если я отключу все встроенные функции и отключить доступ к атрибутам (путем токенизации ввода с помощью токенизатора Python и отклонения его, если он имеет токен доступа к атрибуту)?

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

Я пытаюсь сделать SymPy sympify более безопасным. В настоящее время он токенизирует ввод, выполняет некоторые преобразования на нем и анализирует его в пространстве имен. Но он небезопасен, потому что он позволяет доступ к атрибуту (даже если он ему действительно не нужен).

4b9b3361

Ответ 1

Я собираюсь упомянуть одну из новых функций Python 3.6 - f-строк.

Они могут оценивать выражения,

>>> eval('f"{().__class__.__base__}"', {'__builtins__': None}, {})
"<class 'object'>"

но доступ к атрибуту не будет обнаружен токенизатором Python:

0,0-0,0:            ENCODING       'utf-8'        
1,0-1,1:            ERRORTOKEN     "'"            
1,1-1,27:           STRING         'f"{().__class__.__base__}"'
2,0-2,0:            ENDMARKER      '' 

Ответ 2

Можно построить возвращаемое значение из eval, которое создало бы исключение вне eval, если вы попытались print, log, repr, что угодно:

eval('''((lambda f: (lambda x: x(x))(lambda y: f(lambda *args: y(y)(*args))))
        (lambda f: lambda n: (1,(1,(1,(1,f(n-1))))) if n else 1)(300))''')

Это создает вложенный кортеж формы (1,(1,(1,(1...; это значение не может быть print ed (на Python 3), str ed или repr ed; все попытки его отладки приведут к

RuntimeError: maximum recursion depth exceeded while getting the repr of a tuple

pprint и saferepr тоже не работает:

...
  File "/usr/lib/python3.4/pprint.py", line 390, in _safe_repr
    orepr, oreadable, orecur = _safe_repr(o, context, maxlevels, level)
  File "/usr/lib/python3.4/pprint.py", line 340, in _safe_repr
    if issubclass(typ, dict) and r is dict.__repr__:
RuntimeError: maximum recursion depth exceeded while calling a Python object

Таким образом, нет надежной встроенной функции, чтобы укрепить это: может быть полезен следующий помощник:

def excsafe_repr(obj):
    try:
        return repr(obj)
    except:
        return object.__repr__(obj).replace('>', ' [exception raised]>')

И тогда возникает проблема, заключающаяся в том, что print в Python 2 фактически не использует str/repr, поэтому у вас нет безопасности из-за отсутствия проверок рекурсии. То есть, возьмите возвращаемое значение лямбда-монстра выше, и вы не можете str, repr, но обычный print (не print_function!) Печатает его красиво. Однако вы можете использовать это для создания SIGSEGV на Python 2, если вы знаете, что оно будет напечатано с помощью инструкции print:

print eval('(lambda i: [i for i in ((i, 1) for j in range(1000000))][-1])(1)')

удаляет Python 2 с помощью SIGSEGV. Это WONTFIX в трекере ошибок. Поэтому никогда не используйте print -the-statement, если хотите быть в безопасности. from __future__ import print_function!


Это не сбой, но

eval('(1,' * 100 + ')' * 100)

при запуске, выходы

s_push: parser stack overflow
Traceback (most recent call last):
  File "yyy.py", line 1, in <module>
    eval('(1,' * 100 + ')' * 100)
MemoryError

MemoryError может быть захвачен, является подклассом Exception. У парсера есть действительно консервативные ограничения, чтобы избежать сбоев из stackoverflows (каламбур). Однако s_push: parser stack overflow выводится на stderr с помощью кода C и не может быть подавлен.


И только вчера я спросил почему Python 3.4 не исправлен для сбоя,

% python3  
Python 3.4.3 (default, Mar 26 2015, 22:03:40) 
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class A:
...     def f(self):
...         nonlocal __x
... 
[4]    19173 segmentation fault (core dumped)  python3

и ответ Сергея Сторчака подтвердили, что разработчики ядра Python не считают SIGSEGV по видимому хорошо сформированному коду проблемой безопасности:

Для 3.4 принимаются только исправления безопасности.

Таким образом, можно сделать вывод о том, что никогда нельзя считать безопасным выполнение любого кода от третьего лица в Python, дезинфицированного или нет.

И Nick Coghlan, затем добавлено:

И в качестве дополнительного фона о том, почему ошибки сегментации, спровоцированные кодом Python, в настоящее время не считаются ошибкой безопасности: поскольку CPython не содержит изолированную программную среду безопасности, мы уже полностью полагаемся на ОС, чтобы обеспечить изоляцию процесса. Эта граница безопасности на уровне ОС не зависит от того, работает ли код "нормально" или в измененном состоянии после преднамеренно вызванной ошибки сегментации.

Ответ 3

Пользователи могут по-прежнему выполнять DoS, вводя выражение, которое оценивается до огромного числа, которое заполнило бы вашу память и разбило бы процесс Python, например

'10**10**100'

Мне определенно все еще интересно, возможны ли более традиционные атаки, такие как восстановление встроенных функций или создание segfault.

ИЗМЕНИТЬ:

Получается, что даже этот анализатор Python имеет такую ​​проблему.

lambda: 10**10**100

будет зависать, потому что он пытается прекомпопировать константу.

Ответ 4

Я не считаю, что Python предназначен для защиты от ненадежного кода. Здесь простой способ вызвать segfault через переполнение стека (в стеке C) в официальном интерпретаторе Python 2:

eval('()' * 98765)

Из моего ответа в "Самый короткий код, возвращающий вопрос SIGSEGV" Code Golf.

Ответ 5

Управление словарями locals и globals чрезвычайно важно. В противном случае кто-то может просто передать eval или exec и вызвать его рекурсивно

safe_eval('''e("""[c for c in ().__class__.__base__.__subclasses__() 
    if c.__name__ == \'catch_warnings\'][0]()._module.__builtins__""")''', 
    globals={'e': eval})

Выражение в рекурсивном eval является просто строкой.

Вам также необходимо установить имена eval и exec в глобальном пространстве имен для того, что не является реальным eval или exec. Большое пространство имен важно. Если вы используете локальное пространство имен, все, что создает отдельное пространство имен, например, понимание и лямбда, будет работать вокруг него.

safe_eval('''[eval("""[c for c in ().__class__.__base__.__subclasses__()
    if c.__name__ == \'catch_warnings\'][0]()._module.__builtins__""") for i in [1]][0]''', locals={'eval': None})

safe_eval('''(lambda: eval("""[c for c in ().__class__.__base__.__subclasses__()
    if c.__name__ == \'catch_warnings\'][0]()._module.__builtins__"""))()''',
    locals={'eval': None})

Снова здесь safe_eval видит только строку и вызов функции, а не доступ к атрибутам.

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

safe_eval('safe_eval("<dangerous code>", safe=False)')

Ответ 6

Вот пример safe_eval, который гарантирует, что вычисленное выражение не содержит небезопасных токенов. Он не пытается использовать литературный подход к интерпретации АСТ, а скорее белый список типов токенов и использовать реальный eval, если выражение прошло тест.

# license: MIT (C) tardyp
import ast


def safe_eval(expr, variables):
    """
    Safely evaluate a a string containing a Python
    expression.  The string or node provided may only consist of the following
    Python literal structures: strings, numbers, tuples, lists, dicts, booleans,
    and None. safe operators are allowed (and, or, ==, !=, not, +, -, ^, %, in, is)
    """
    _safe_names = {'None': None, 'True': True, 'False': False}
    _safe_nodes = [
        'Add', 'And', 'BinOp', 'BitAnd', 'BitOr', 'BitXor', 'BoolOp',
        'Compare', 'Dict', 'Eq', 'Expr', 'Expression', 'For',
        'Gt', 'GtE', 'Is', 'In', 'IsNot', 'LShift', 'List',
        'Load', 'Lt', 'LtE', 'Mod', 'Name', 'Not', 'NotEq', 'NotIn',
        'Num', 'Or', 'RShift', 'Set', 'Slice', 'Str', 'Sub',
        'Tuple', 'UAdd', 'USub', 'UnaryOp', 'boolop', 'cmpop',
        'expr', 'expr_context', 'operator', 'slice', 'unaryop']
    node = ast.parse(expr, mode='eval')
    for subnode in ast.walk(node):
        subnode_name = type(subnode).__name__
        if isinstance(subnode, ast.Name):
            if subnode.id not in _safe_names and subnode.id not in variables:
                raise ValueError("Unsafe expression {}. contains {}".format(expr, subnode.id))
        if subnode_name not in _safe_nodes:
            raise ValueError("Unsafe expression {}. contains {}".format(expr, subnode_name))

    return eval(expr, variables)



class SafeEvalTests(unittest.TestCase):

    def test_basic(self):
        self.assertEqual(safe_eval("1", {}), 1)

    def test_local(self):
        self.assertEqual(safe_eval("a", {'a': 2}), 2)

    def test_local_bool(self):
        self.assertEqual(safe_eval("a==2", {'a': 2}), True)

    def test_lambda(self):
        self.assertRaises(ValueError, safe_eval, "lambda : None", {'a': 2})

    def test_bad_name(self):
        self.assertRaises(ValueError, safe_eval, "a == None2", {'a': 2})

    def test_attr(self):
        self.assertRaises(ValueError, safe_eval, "a.__dict__", {'a': 2})

    def test_eval(self):
        self.assertRaises(ValueError, safe_eval, "eval('os.exit()')", {})

    def test_exec(self):
        self.assertRaises(SyntaxError, safe_eval, "exec 'import os'", {})

    def test_multiply(self):
        self.assertRaises(ValueError, safe_eval, "'s' * 3", {})

    def test_power(self):
        self.assertRaises(ValueError, safe_eval, "3 ** 3", {})

    def test_comprehensions(self):
        self.assertRaises(ValueError, safe_eval, "[i for i in [1,2]]", {'i': 1})