La razón eval
y exec
son tan peligrosos es que la compile
función predeterminada generará un código de bytes para cualquier expresión válida de Python, y la predeterminada eval
o exec
ejecutará cualquier código de bytes de Python válido. Todas las respuestas hasta la fecha se han centrado en restringir el código de bytes que se puede generar (desinfectando la entrada) o en la construcción de su propio lenguaje específico de dominio utilizando el AST.
En su lugar, puede crear fácilmente una eval
función simple que es incapaz de hacer nada nefasto y puede tener fácilmente verificaciones de tiempo de ejecución en la memoria o el tiempo utilizado. Por supuesto, si se trata de matemáticas simples, entonces hay un atajo.
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]
La forma en que esto funciona es simple, cualquier expresión matemática constante se evalúa de manera segura durante la compilación y se almacena como una constante. El objeto de código devuelto por la compilación consta de d
, cuál es el código de bytes para LOAD_CONST
, seguido del número de la constante a cargar (generalmente el último en la lista), seguido de S
, que es el código de bytes para RETURN_VALUE
. Si este atajo no funciona, significa que la entrada del usuario no es una expresión constante (contiene una variable o llamada de función o similar).
Esto también abre la puerta a algunos formatos de entrada más sofisticados. Por ejemplo:
stringExp = "1 + cos(2)"
Esto requiere evaluar realmente el código de bytes, que aún es bastante simple. El código de bytes de Python es un lenguaje orientado a pilas, por lo que todo es una cuestión simple TOS=stack.pop(); op(TOS); stack.put(TOS)
o similar. La clave es implementar solo los códigos de operación que son seguros (carga / almacenamiento de valores, operaciones matemáticas, valores de retorno) y no inseguros (búsqueda de atributos). Si desea que el usuario pueda llamar a funciones (la CALL_FUNCTION
única razón para no usar el atajo anterior), simplemente haga su implementación de solo permitir funciones en una lista 'segura'.
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'))
Obviamente, la versión real de esto sería un poco más larga (hay 119 códigos de operación, 24 de los cuales están relacionados con las matemáticas). Agregar STORE_FAST
y un par más permitiría una entrada similar 'x=5;return x+x
o similar, de manera trivialmente fácil. Incluso se puede usar para ejecutar funciones creadas por el usuario, siempre que las funciones creadas por el usuario se ejecuten a través de VMeval (¡no las haga invocables! O podrían usarse como devolución de llamada en algún lugar). El manejo de bucles requiere soporte para los goto
códigos de bytes, lo que significa cambiar de un for
iterador while
y mantener un puntero a la instrucción actual, pero no es demasiado difícil. Para resistir a DOS, el bucle principal debe verificar cuánto tiempo ha pasado desde el inicio del cálculo, y ciertos operadores deben negar la entrada por encima de algún límite razonable (BINARY_POWER
siendo el más obvio).
Si bien este enfoque es algo más largo que un simple analizador gramatical para expresiones simples (vea más arriba acerca de simplemente tomar la constante compilada), se extiende fácilmente a entradas más complicadas y no requiere lidiar con la gramática ( compile
tome cualquier cosa arbitrariamente complicada y reduzca a una secuencia de instrucciones sencillas).