Der Grund eval
und exec
die Gefahr besteht darin, dass die Standardfunktion compile
Bytecode für jeden gültigen Python-Ausdruck generiert, und die Standardfunktion eval
oderexec
einen gültigen Python-Bytecode ausführt ausführt. Alle bisherigen Antworten konzentrierten sich auf die Einschränkung des Bytecodes, der generiert werden kann (durch Bereinigen der Eingabe) oder das Erstellen einer eigenen domänenspezifischen Sprache mithilfe des AST.
Stattdessen können Sie auf einfache Weise eine einfache eval
Funktion erstellen , die nicht in der Lage ist, etwas Schändliches zu tun, und auf einfache Weise Laufzeitprüfungen des Speichers oder der verwendeten Zeit durchführen lassen. Wenn es sich um einfache Mathematik handelt, gibt es natürlich eine Abkürzung.
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]
Die Funktionsweise ist einfach: Jeder konstante mathematische Ausdruck wird während der Kompilierung sicher ausgewertet und als Konstante gespeichert. Das von compile zurückgegebene Codeobjekt besteht aus d
dem Bytecode für LOAD_CONST
, gefolgt von der Nummer der zu ladenden Konstante (normalerweise die letzte in der Liste), gefolgt vonS
dem Bytecode für RETURN_VALUE
. Wenn diese Verknüpfung nicht funktioniert, bedeutet dies, dass die Benutzereingabe kein konstanter Ausdruck ist (enthält eine Variable oder einen Funktionsaufruf oder ähnliches).
Dies öffnet auch die Tür zu einigen komplexeren Eingabeformaten. Beispielsweise:
stringExp = "1 + cos(2)"
Dies erfordert die tatsächliche Auswertung des Bytecodes, was immer noch recht einfach ist. Python-Bytecode ist eine stapelorientierte Sprache, daher ist alles einfach TOS=stack.pop(); op(TOS); stack.put(TOS)
oder ähnlich. Der Schlüssel besteht darin, nur die Opcodes zu implementieren, die sicher sind (Laden / Speichern von Werten, mathematische Operationen, Zurückgeben von Werten) und nicht unsichere (Attributsuche). Wenn Sie möchten, dass der Benutzer Funktionen aufrufen kann (der ganze Grund, die obige Verknüpfung nicht zu verwenden), können Sie einfach CALL_FUNCTION
nur zulässige Funktionen in einer "sicheren" Liste implementieren .
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'))
Offensichtlich wäre die eigentliche Version etwas länger (es gibt 119 Opcodes, von denen 24 mathematisch sind). Das Hinzufügen STORE_FAST
und ein paar andere würden eine Eingabe wie 'x=5;return x+x
oder ähnlich ermöglichen, trivial einfach. Es kann sogar verwendet werden, um vom Benutzer erstellte Funktionen auszuführen, solange die vom Benutzer erstellten Funktionen selbst über VMeval ausgeführt werden (machen Sie sie nicht aufrufbar !!! oder sie könnten irgendwo als Rückruf verwendet werden). Die Behandlung von Schleifen erfordert die Unterstützung der goto
Bytecodes, was bedeutet, dass der Wechsel von einem Wesen am offensichtlichsten ist.for
Iterator zu while
einem Zeiger auf den aktuellen Befehl und dieser beibehalten wird, dies ist jedoch nicht zu schwierig. Für den Widerstand gegen DOS sollte die Hauptschleife prüfen, wie viel Zeit seit Beginn der Berechnung vergangen ist, und bestimmte Operatoren sollten die Eingabe über einen angemessenen Grenzwert verweigern (BINARY_POWER
Dieser Ansatz ist zwar etwas länger als ein einfacher Grammatik-Parser für einfache Ausdrücke (siehe oben, in dem nur die kompilierte Konstante erfasst wird), erstreckt sich jedoch leicht auf kompliziertere Eingaben und erfordert keine Behandlung der Grammatik ( compile
nehmen Sie etwas willkürlich Kompliziertes und reduzieren Sie es auf eine Folge einfacher Anweisungen).