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