Powodem eval
i exec
są tak niebezpieczne jest to, że compile
funkcja domyślna wygeneruje kod bajtowy dla dowolnego prawidłowego wyrażenia Pythona, a domyślny eval
lub exec
wykona dowolny prawidłowy kod bajtowy Pythona. Wszystkie dotychczasowe odpowiedzi skupiały się na ograniczaniu kodu bajtowego, który może być generowany (poprzez odkażanie danych wejściowych) lub budowaniu własnego języka specyficznego dla domeny za pomocą AST.
Zamiast tego możesz łatwo utworzyć prostą eval
funkcję, która nie jest w stanie zrobić niczego nikczemnego i może łatwo sprawdzić w czasie wykonywania pamięć lub wykorzystany czas. Oczywiście, jeśli jest to prosta matematyka, istnieje skrót.
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]
Sposób, w jaki to działa, jest prosty, każde stałe wyrażenie matematyczne jest bezpiecznie oceniane podczas kompilacji i przechowywane jako stała. Obiekt kodu zwrócony przez kompilację składa się z d
kodu bajtowego dla LOAD_CONST
, po którym następuje numer stałej do załadowania (zwykle ostatnia na liście), po S
którym następuje kod bajtowy dla RETURN_VALUE
. Jeśli ten skrót nie działa, oznacza to, że dane wejściowe użytkownika nie są wyrażeniem stałym (zawierają zmienną, wywołanie funkcji lub podobne).
Otwiera to również drzwi do bardziej wyrafinowanych formatów wejściowych. Na przykład:
stringExp = "1 + cos(2)"
Wymaga to rzeczywistej oceny kodu bajtowego, co nadal jest dość proste. Kod bajtowy Pythona jest językiem zorientowanym na stos, więc wszystko jest proste TOS=stack.pop(); op(TOS); stack.put(TOS)
lub podobne. Kluczem jest implementacja tylko tych opkodów, które są bezpieczne (ładowanie / przechowywanie wartości, operacje matematyczne, zwracanie wartości), a nie niebezpiecznych (wyszukiwanie atrybutów). Jeśli chcesz, aby użytkownik mógł wywoływać funkcje (cały powód, aby nie używać powyższego skrótu), w prosty sposób wprowadź w życie CALL_FUNCTION
tylko zezwalanie na funkcje na liście „bezpiecznych”.
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'))
Oczywiście rzeczywista wersja byłaby nieco dłuższa (jest 119 rozkazów, z których 24 są związane z matematyką). Dodanie STORE_FAST
i kilka innych pozwoliłoby na wprowadzenie podobnych 'x=5;return x+x
lub podobnych, trywialnie łatwo. Może być nawet używany do wykonywania funkcji utworzonych przez użytkownika, o ile funkcje utworzone przez użytkownika są wykonywane przez VMeval (nie należy ich wywoływać !!! w przeciwnym razie mogą zostać użyte jako wywołanie zwrotne). Obsługa pętli wymaga obsługi goto
kodów bajtowych, co oznacza zmianę z for
iteratora while
na bieżącą instrukcję i utrzymywanie wskaźnika do bieżącej instrukcji, ale nie jest to zbyt trudne. Aby uzyskać odporność na DOS, główna pętla powinna sprawdzać, ile czasu minęło od rozpoczęcia obliczeń, a niektórzy operatorzy powinni odmawiać wprowadzania danych powyżej rozsądnego limitu (BINARY_POWER
jest najbardziej oczywiste).
Chociaż to podejście jest nieco dłuższe niż prosty parser gramatyczny dla prostych wyrażeń (patrz wyżej o zwykłym przechwytywaniu skompilowanej stałej), rozciąga się łatwo na bardziej skomplikowane dane wejściowe i nie wymaga zajmowania się gramatyką ( compile
weź wszystko, co jest dowolnie skomplikowane i redukuje je do sekwencja prostych instrukcji).