Причина 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
возьмите что-нибудь произвольно сложное и сведите его к последовательность простых инструкций).