Причина evalи execнастолько опасны, что compileфункция по умолчанию будет генерировать байт-код для любого допустимого выражения python, а по умолчанию evalили execбудет выполнять любой действительный байт-код python. Все ответы на сегодняшний день сосредоточены на ограничении байт-кода, который может быть сгенерирован (путем дезинфекции ввода), или создании собственного языка для конкретной предметной области с использованием AST.
Вместо этого вы можете легко создать простую evalфункцию, которая не способна делать что-либо гнусное и может легко выполнять проверки памяти или времени. Конечно, если это простая математика, тогда есть ярлык.
c = compile(stringExp, 'userinput', 'eval')
if c.co_code[0]==b'd' and c.co_code[3]==b'S':
return c.co_consts[ord(c.co_code[1])+ord(c.co_code[2])*256]
Это работает просто: любое математическое выражение константы безопасно вычисляется во время компиляции и сохраняется как константа. Объект кода, возвращаемый компиляцией, состоит из dбайт-кода, за LOAD_CONSTкоторым следует номер загружаемой константы (обычно последней в списке), за Sкоторым следует байт-код для RETURN_VALUE. Если этот ярлык не работает, это означает, что вводимые пользователем данные не являются постоянным выражением (содержат переменную или вызов функции или подобное).
Это также открывает двери для некоторых более сложных форматов ввода. Например:
stringExp = "1 + cos(2)"
Это требует фактической оценки байт-кода, что все еще довольно просто. Байт-код Python - это язык, ориентированный на стек, поэтому все просто TOS=stack.pop(); op(TOS); stack.put(TOS)или похоже. Ключ состоит в том, чтобы реализовать только безопасные коды операций (загрузка / сохранение значений, математические операции, возврат значений), а не небезопасные (поиск атрибутов). Если вы хотите, чтобы пользователь мог вызывать функции (вся причина не использовать приведенный выше ярлык), просто сделайте свою реализацию CALL_FUNCTIONтолько разрешающих функций в «безопасном» списке.
from dis import opmap
from Queue import LifoQueue
from math import sin,cos
import operator
globs = {'sin':sin, 'cos':cos}
safe = globs.values()
stack = LifoQueue()
class BINARY(object):
def __init__(self, operator):
self.op=operator
def __call__(self, context):
stack.put(self.op(stack.get(),stack.get()))
class UNARY(object):
def __init__(self, operator):
self.op=operator
def __call__(self, context):
stack.put(self.op(stack.get()))
def CALL_FUNCTION(context, arg):
argc = arg[0]+arg[1]*256
args = [stack.get() for i in range(argc)]
func = stack.get()
if func not in safe:
raise TypeError("Function %r now allowed"%func)
stack.put(func(*args))
def LOAD_CONST(context, arg):
cons = arg[0]+arg[1]*256
stack.put(context['code'].co_consts[cons])
def LOAD_NAME(context, arg):
name_num = arg[0]+arg[1]*256
name = context['code'].co_names[name_num]
if name in context['locals']:
stack.put(context['locals'][name])
else:
stack.put(context['globals'][name])
def RETURN_VALUE(context):
return stack.get()
opfuncs = {
opmap['BINARY_ADD']: BINARY(operator.add),
opmap['UNARY_INVERT']: UNARY(operator.invert),
opmap['CALL_FUNCTION']: CALL_FUNCTION,
opmap['LOAD_CONST']: LOAD_CONST,
opmap['LOAD_NAME']: LOAD_NAME
opmap['RETURN_VALUE']: RETURN_VALUE,
}
def VMeval(c):
context = dict(locals={}, globals=globs, code=c)
bci = iter(c.co_code)
for bytecode in bci:
func = opfuncs[ord(bytecode)]
if func.func_code.co_argcount==1:
ret = func(context)
else:
args = ord(bci.next()), ord(bci.next())
ret = func(context, args)
if ret:
return ret
def evaluate(expr):
return VMeval(compile(expr, 'userinput', 'eval'))
Очевидно, настоящая версия этого была бы немного длиннее (существует 119 кодов операций, 24 из которых связаны с математикой). Добавление STORE_FASTи пара других позволят вводить подобные 'x=5;return x+xили похожие, тривиально легко. Его даже можно использовать для выполнения функций, созданных пользователем, при условии, что созданные пользователем функции сами выполняются через VMeval (не делайте их вызываемыми !!! или они могут где-то использоваться как обратный вызов). Обработка циклов требует поддержки gotoбайт-кодов, что означает переход с forитератора whileна текущую инструкцию и поддержание указателя на нее, но это не слишком сложно. Для обеспечения устойчивости к DOS основной цикл должен проверять, сколько времени прошло с начала вычисления, и некоторые операторы должны отклонять ввод сверх некоторого разумного предела (BINARY_POWER самый очевидный).
Хотя этот подход несколько длиннее, чем простой синтаксический анализатор грамматики для простых выражений (см. Выше о простом захвате скомпилированной константы), он легко распространяется на более сложный ввод и не требует работы с грамматикой ( compileвозьмите что-нибудь произвольно сложное и сведите его к последовательность простых инструкций).