Il motivo eval
e exec
sono così pericolosi è che la compile
funzione predefinita genererà bytecode per qualsiasi espressione python valida e il valore predefinito eval
oexec
eseguirà qualsiasi bytecode python valido. Tutte le risposte fino ad oggi si sono concentrate sulla limitazione del bytecode che può essere generato (disinfettando l'input) o sulla costruzione del proprio linguaggio specifico del dominio utilizzando l'AST.
Invece, puoi facilmente creare una semplice eval
funzione che è incapace di fare qualcosa di nefasto e può facilmente avere controlli di runtime sulla memoria o sul tempo utilizzato. Ovviamente, se è semplice matematica, c'è una scorciatoia.
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]
Il modo in cui funziona è semplice, qualsiasi espressione matematica costante viene valutata in modo sicuro durante la compilazione e memorizzata come costante. L'oggetto codice restituito da compile è costituito da d
, che è il bytecode LOAD_CONST
, seguito dal numero della costante da caricare (di solito l'ultima nell'elenco), seguito daS
, che è il bytecode per RETURN_VALUE
. Se questa scorciatoia non funziona, significa che l'input dell'utente non è un'espressione costante (contiene una variabile o una chiamata di funzione o simili).
Questo apre anche la porta ad alcuni formati di input più sofisticati. Per esempio:
stringExp = "1 + cos(2)"
Ciò richiede effettivamente la valutazione del bytecode, che è ancora abbastanza semplice. Il bytecode Python è un linguaggio orientato allo stack, quindi tutto è una questione semplice TOS=stack.pop(); op(TOS); stack.put(TOS)
o simile. La chiave è implementare solo i codici operativi sicuri (caricamento / memorizzazione di valori, operazioni matematiche, restituzione di valori) e non non sicuri (ricerca degli attributi). Se vuoi che l'utente sia in grado di chiamare le funzioni (l'intera ragione per non usare la scorciatoia sopra), semplifica l'implementazione di CALL_FUNCTION
consentire solo le funzioni in un elenco "sicuro".
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'))
Ovviamente, la versione reale di questo sarebbe un po 'più lunga (ci sono 119 opcode, 24 dei quali sono legati alla matematica). L'aggiunta STORE_FAST
e un paio di altri consentirebbero input simili 'x=5;return x+x
o simili, banalmente facilmente. Può anche essere usato per eseguire funzioni create dall'utente, a patto che le funzioni create dall'utente siano esse stesse eseguite tramite VMeval (non renderle richiamabili !!! o potrebbero essere usate come callback da qualche parte). La gestione dei loop richiede il supporto per i goto
bytecode, il che significa cambiare da un essere il più ovvio).for
iteratore a while
mantenere un puntatore all'istruzione corrente, ma non è troppo difficile. Per resistere al DOS, il ciclo principale dovrebbe controllare quanto tempo è passato dall'inizio del calcolo e alcuni operatori dovrebbero negare l'input oltre un limite ragionevole (BINARY_POWER
Sebbene questo approccio sia un po 'più lungo di un semplice analizzatore grammaticale per espressioni semplici (vedi sopra per compile
prendere semplicemente la costante compilata), si estende facilmente a input più complicati e non richiede di occuparsi della grammatica ( prendi qualsiasi cosa arbitrariamente complicata e la riduce a una sequenza di semplici istruzioni).