¿Por qué usar 'eval' es una mala práctica?

138

Estoy usando la siguiente clase para almacenar fácilmente datos de mis canciones.

class Song:
    """The class to store the details of each song"""
    attsToStore=('Name', 'Artist', 'Album', 'Genre', 'Location')
    def __init__(self):
        for att in self.attsToStore:
            exec 'self.%s=None'%(att.lower()) in locals()
    def setDetail(self, key, val):
        if key in self.attsToStore:
            exec 'self.%s=val'%(key.lower()) in locals()

Siento que esto es mucho más extensible que escribir un if/elsebloque. Sin embargo, evalparece ser considerado una mala práctica e inseguro de usar. Si es así, ¿alguien puede explicarme por qué y mostrarme una mejor manera de definir la clase anterior?

Nikwin
fuente
40
¿Cómo se enteró exec/evaly aún no lo sabía setattr?
u0b34a0f6ae
3
Creo que fue de un artículo que compara python y lisp de lo que aprendí sobre eval.
Nikwin

Respuestas:

194

Sí, usar eval es una mala práctica. Solo por nombrar algunas razones:

  1. Casi siempre hay una mejor manera de hacerlo.
  2. Muy peligroso e inseguro
  3. Hace que la depuración sea difícil
  4. Lento

En su caso, puede usar setattr en su lugar:

class Song:
    """The class to store the details of each song"""
    attsToStore=('Name', 'Artist', 'Album', 'Genre', 'Location')
    def __init__(self):
        for att in self.attsToStore:
            setattr(self, att.lower(), None)
    def setDetail(self, key, val):
        if key in self.attsToStore:
            setattr(self, key.lower(), val)

EDITAR:

Hay algunos casos en los que tiene que usar eval o exec. Pero son raros. Usar eval en su caso es una mala práctica con seguridad. Estoy haciendo hincapié en las malas prácticas porque eval y exec se usan con frecuencia en el lugar equivocado.

EDITAR 2:

Parece un poco en desacuerdo que eval sea 'muy peligroso e inseguro' en el caso de OP. Eso podría ser cierto para este caso específico, pero no en general. La pregunta era general y las razones que enumeré también son ciertas para el caso general.

EDITAR 3: reordenó los puntos 1 y 4

Nadia Alramli
fuente
23
-1: "Muy peligroso e inseguro" es falso. Los otros tres son extraordinariamente claros. Reordenarlos para que 2 y 4 sean los dos primeros. Solo es inseguro si estás rodeado de sociópatas malvados que buscan formas de subvertir tu aplicación.
S.Lott
51
@ S.Lott, la inseguridad es una razón muy importante para evitar eval / exec en general. Muchas aplicaciones como los sitios web deben tener mucho cuidado. Tome el ejemplo de OP en un sitio web que espera que los usuarios ingresen el nombre de la canción. Está destinado a ser explotado tarde o temprano. Incluso una aportación inocente como: divirtámonos. provocará un error de sintaxis y expondrá la vulnerabilidad.
Nadia Alramli
18
@ Nadia Alramli: entrada del usuario y evalno tiene nada que ver el uno con el otro. Una aplicación que está fundamentalmente mal diseñada está fundamentalmente mal diseñada. evalLa causa del mal diseño no es más que la división por cero o el intento de importar un módulo que se sabe que no existe. evalNo es inseguro. Las aplicaciones son inseguras.
S.Lott
17
@jeffjose: En realidad, es fundamentalmente malo / malo porque trata los datos no parametrizados como código (es por eso que existen XSS, inyección SQL y bloqueos de pila). @ S.Lott: "Solo es inseguro si estás rodeado de sociópatas malvados que buscan formas de subvertir tu aplicación". Genial, digamos que haces un programa calc, y para agregar números se ejecuta print(eval("{} + {}".format(n1, n2)))y sale. Ahora distribuyes este programa con algunos SO. Luego, alguien crea un script bash que toma algunos números de un sitio web y los agrega usando calc. ¿auge?
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
57
No estoy seguro de por qué la afirmación de Nadia es tan polémica. Me parece simple: eval es un vector para la inyección de código, y es peligroso de una manera que la mayoría de las otras funciones de Python no lo son. Eso no significa que no debas usarlo en absoluto, pero creo que debes usarlo juiciosamente.
Owen S.
32

Usar evales débil, no es una práctica claramente mala .

  1. Viola el "Principio fundamental del software". Su fuente no es la suma total de lo que es ejecutable. Además de su fuente, existen argumentos evalque deben entenderse claramente. Por esta razón, es la herramienta de último recurso.

  2. Suele ser un signo de diseño irreflexivo. Rara vez hay una buena razón para el código fuente dinámico, construido sobre la marcha. Casi cualquier cosa se puede hacer con delegación y otras técnicas de diseño OO.

  3. Conduce a una compilación relativamente lenta sobre la marcha de pequeñas piezas de código. Una sobrecarga que puede evitarse utilizando mejores patrones de diseño.

Como nota al pie, en manos de sociópatas trastornados, puede que no funcione bien. Sin embargo, cuando se enfrentan a usuarios o administradores sociópatas trastornados, es mejor no darles Python interpretado en primer lugar. En manos de los verdaderamente malvados, Python puede ser una responsabilidad; evalno aumenta el riesgo en absoluto.

S.Lott
fuente
77
@Owen S. El punto es este. La gente le dirá que evales algún tipo de "vulnerabilidad de seguridad". Como si Python, en sí mismo, no fuera solo un montón de fuentes interpretadas que cualquiera podría modificar. Cuando te enfrentas a la "evaluación es un agujero de seguridad", solo puedes asumir que es un agujero de seguridad en manos de sociópatas. Los programadores ordinarios simplemente modifican la fuente Python existente y causan sus problemas directamente. No indirectamente a través de la evalmagia.
S.Lott
14
Bueno, puedo decirte exactamente por qué diría que eval es una vulnerabilidad de seguridad, y tiene que ver con la confiabilidad de la cadena que se proporciona como entrada. Si esa cadena proviene, en todo o en parte, del mundo exterior, existe la posibilidad de un ataque de secuencias de comandos en su programa si no tiene cuidado. Pero eso es el trastorno de un atacante externo, no del usuario o administrador.
Owen S.
66
@OwenS .: "Si esa cadena proviene, total o parcialmente, del mundo exterior" A menudo es falsa. Esto no es una cosa "cuidadosa". Es blanco y negro. Si el texto proviene de un usuario, nunca se puede confiar. El cuidado no es realmente parte de esto, es absolutamente imposible de confiar. De lo contrario, el texto proviene de un desarrollador, instalador o administrador, y se puede confiar en él.
S.Lott
8
@OwenS .: no es posible escapar de una cadena de código de Python no confiable que lo haría confiable. Estoy de acuerdo con la mayoría de lo que estás diciendo, excepto por la parte "cuidadosa". Es una distinción muy nítida. El código del mundo exterior no es confiable. AFAIK, ninguna cantidad de escape o filtrado puede limpiarlo. Si tiene algún tipo de función de escape que haría que el código sea aceptable, por favor comparta. No pensé que tal cosa fuera posible. Por ejemplo while True: pass, sería difícil de limpiar con algún tipo de escape.
S.Lott
2
@OwenS .: "destinado a ser una cadena, no un código arbitrario". Eso no está relacionado. Eso es solo un valor de cadena, que nunca pasarías eval(), ya que es una cadena. El código del "mundo exterior" no se puede desinfectar. Las cuerdas del mundo exterior son solo cuerdas. No tengo claro de qué estás hablando. Tal vez debería proporcionar una publicación de blog más completa y un enlace a ella aquí.
S.Lott
23

En este caso, si. En vez de

exec 'self.Foo=val'

deberías usar la función incorporadasetattr :

setattr(self, 'Foo', val)
Josh Lee
fuente
16

Sí lo es:

Hackear usando Python:

>>> eval(input())
"__import__('os').listdir('.')"
...........
...........   #dir listing
...........

El siguiente código enumerará todas las tareas que se ejecutan en una máquina con Windows.

>>> eval(input())
"__import__('subprocess').Popen(['tasklist'],stdout=__import__('subprocess').PIPE).communicate()[0]"

En Linux:

>>> eval(input())
"__import__('subprocess').Popen(['ps', 'aux'],stdout=__import__('subprocess').PIPE).communicate()[0]"
Adicto al alcohol
fuente
7

Vale la pena señalar que para el problema específico en cuestión, hay varias alternativas para usar eval:

Lo más simple, como se señaló, es usar setattr:

def __init__(self):
    for name in attsToStore:
        setattr(self, name, None)

Un enfoque menos obvio es actualizar el objeto del __dict__objeto directamente. Si todo lo que quiere hacer es inicializar los atributos None, entonces esto es menos sencillo que lo anterior. Pero considera esto:

def __init__(self, **kwargs):
    for name in self.attsToStore:
       self.__dict__[name] = kwargs.get(name, None)

Esto le permite pasar argumentos de palabras clave al constructor, por ejemplo:

s = Song(name='History', artist='The Verve')

También le permite hacer su uso locals()más explícito, por ejemplo:

s = Song(**locals())

... y, si realmente desea asignar Nonea los atributos cuyos nombres se encuentran en locals():

s = Song(**dict([(k, None) for k in locals().keys()]))

Otro enfoque para proporcionar un objeto con valores predeterminados para una lista de atributos es definir el __getattr__método de la clase :

def __getattr__(self, name):
    if name in self.attsToStore:
        return None
    raise NameError, name

Este método recibe una llamada cuando el atributo nombrado no se encuentra de la manera normal. Este enfoque es algo menos directo que simplemente establecer los atributos en el constructor o actualizar el __dict__, pero tiene el mérito de no crear realmente el atributo a menos que exista, lo que puede reducir sustancialmente el uso de memoria de la clase.

El punto de todo esto: hay muchas razones, en general, para evitar eval: el problema de seguridad de ejecutar código que no controlas, el problema práctico de código que no puedes depurar, etc. Pero una razón aún más importante es que generalmente no necesitas usarlo. Python expone gran parte de sus mecanismos internos al programador que rara vez realmente necesita escribir código que escriba código.

Robert Rossney
fuente
1
Otra forma que podría decirse que es más (o menos) Pythonic: en lugar de usar los objetos __dict__directamente, dele al objeto un objeto de diccionario real, ya sea por herencia o como un atributo.
Josh Lee
1
"Un enfoque menos obvio es actualizar el objeto dict del objeto directamente" => Tenga en cuenta que esto omitirá cualquier descriptor (propiedad u otro) o __setattr__anulación, lo que podría conducir a resultados inesperados. setattr()no tiene este problema
bruno desthuilliers
5

Otros usuarios señalaron cómo se puede cambiar su código para no depender de él eval; Ofreceré un caso de uso legítimo para usar eval, uno que se encuentra incluso en CPython: pruebas .

Aquí hay un ejemplo que encontré en test_unary.pydonde una prueba sobre si (+|-|~)b'a'plantea un TypeError:

def test_bad_types(self):
    for op in '+', '-', '~':
        self.assertRaises(TypeError, eval, op + "b'a'")
        self.assertRaises(TypeError, eval, op + "'a'")

El uso claramente no es una mala práctica aquí; Usted define la entrada y simplemente observa el comportamiento. evalEs útil para las pruebas.

Echar un vistazo a esta búsqueda para eval, realizado en el repositorio git CPython; las pruebas con eval se usan mucho.

Dimitris Fasarakis Hilliard
fuente
2

Cuando eval()se usa para procesar la entrada proporcionada por el usuario, usted habilita al usuario para soltar a REPL proporcionando algo como esto:

"__import__('code').InteractiveConsole(locals=globals()).interact()"

Puede salirse con la suya, pero normalmente no desea vectores para la ejecución de código arbitrario en sus aplicaciones.

moooeeeep
fuente
1

Además de la respuesta de @Nadia Alramli, como soy nuevo en Python y estaba ansioso por comprobar cómo el uso evalafectará los tiempos , probé un pequeño programa y a continuación se muestran las observaciones:

#Difference while using print() with eval() and w/o eval() to print an int = 0.528969s per 100000 evals()

from datetime import datetime
def strOfNos():
    s = []
    for x in range(100000):
        s.append(str(x))
    return s

strOfNos()
print(datetime.now())
for x in strOfNos():
    print(x) #print(eval(x))
print(datetime.now())

#when using eval(int)
#2018-10-29 12:36:08.206022
#2018-10-29 12:36:10.407911
#diff = 2.201889 s

#when using int only
#2018-10-29 12:37:50.022753
#2018-10-29 12:37:51.090045
#diff = 1.67292
Sastre Lokeshwar
fuente