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