Pourquoi utiliser «eval» est-il une mauvaise pratique?


138

J'utilise la classe suivante pour stocker facilement les données de mes chansons.

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()

Je pense que c'est juste beaucoup plus extensible que d'écrire un if/elsebloc. Cependant, evalsemble être considéré comme une mauvaise pratique et dangereux à utiliser. Si oui, quelqu'un peut-il m'expliquer pourquoi et me montrer une meilleure façon de définir la classe ci-dessus?


40
comment avez-vous appris exec/evalet ne saviez toujours pas setattr?
u0b34a0f6ae

3
Je crois que c'est à partir d'un article comparant python et lisp que j'ai appris sur eval.
Nikwin

Réponses:


194

Oui, utiliser eval est une mauvaise pratique. Pour ne citer que quelques raisons:

  1. Il y a presque toujours une meilleure façon de le faire
  2. Très dangereux et peu sûr
  3. Rend le débogage difficile
  4. Lent

Dans votre cas, vous pouvez utiliser setattr à la place:

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)

ÉDITER:

Dans certains cas, vous devez utiliser eval ou exec. Mais ils sont rares. Utiliser eval dans votre cas est certainement une mauvaise pratique. Je mets l'accent sur les mauvaises pratiques car eval et exec sont souvent utilisés au mauvais endroit.

MODIFIER 2:

Il semble que certains ne soient pas d'accord sur le fait que l'évaluation est «très dangereuse et peu sûre» dans le cas de l'OP. Cela pourrait être vrai pour ce cas précis, mais pas en général. La question était générale et les raisons que j'ai énumérées sont également valables pour le cas général.

EDIT 3: réorganisé les points 1 et 4


23
-1: "Très dangereux et peu sûr" est faux. Les trois autres sont extrêmement clairs. Veuillez les réorganiser afin que 2 et 4 soient les deux premiers. Ce n'est pas sûr que vous êtes entouré de sociopathes pervers qui cherchent des moyens de subvertir votre candidature.
S.Lott

51
@ S.Lott, l'insécurité est une raison très importante pour éviter eval / exec en général. De nombreuses applications telles que les sites Web doivent faire particulièrement attention. Prenons l'exemple de l'OP sur un site Web qui attend des utilisateurs qu'ils entrent le nom de la chanson. Il est appelé à être exploité tôt ou tard. Même une entrée innocente comme: Amusons-nous. provoquera une erreur de syntaxe et exposera la vulnérabilité.
Nadia Alramli

18
@Nadia Alramli: les entrées de l'utilisateur et evaln'ont rien à voir les unes avec les autres. Une application qui est fondamentalement mal conçue est fondamentalement mal conçue. evaln'est pas plus la cause première d'une mauvaise conception que la division par zéro ou la tentative d'importer un module dont on sait qu'il n'existe pas. evaln'est pas incertain. Les applications ne sont pas sécurisées.
S.Lott

17
@jeffjose: En fait, c'est fondamentalement mauvais / mauvais parce qu'il traite les données non paramétrées comme du code (c'est pourquoi XSS, l'injection SQL et les smashs de pile existent). @ S.Lott: "Ce n'est pas sûr que si vous êtes entouré de sociopathes pervers qui cherchent des moyens de subvertir votre candidature." Cool, disons que vous créez un programme calc, et que pour ajouter des numéros, il s'exécute print(eval("{} + {}".format(n1, n2)))et quitte. Maintenant, vous distribuez ce programme avec certains OS. Ensuite, quelqu'un crée un script bash qui prend des nombres d'un site de stock et les ajoute à l'aide de calc. boom?
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

57
Je ne sais pas pourquoi l'affirmation de Nadia est si controversée. Cela me semble simple: eval est un vecteur d'injection de code, et il est dangereux d'une manière que la plupart des autres fonctions Python ne le sont pas. Cela ne signifie pas que vous ne devriez pas l'utiliser du tout, mais je pense que vous devriez l'utiliser judicieusement.
Owen S.

32

L'utilisation evalest faible, ce n'est clairement pas une mauvaise pratique.

  1. Il viole le «principe fondamental du logiciel». Votre source n'est pas la somme totale de ce qui est exécutable. En plus de votre source, il y a les arguments pour eval, qui doivent être clairement compris. Pour cette raison, c'est l'outil de dernier recours.

  2. C'est généralement le signe d'un design irréfléchi. Il y a rarement une bonne raison pour un code source dynamique, construit à la volée. Presque tout peut être fait avec la délégation et d'autres techniques de conception OO.

  3. Cela conduit à une compilation à la volée relativement lente de petits morceaux de code. Une surcharge qui peut être évitée en utilisant de meilleurs modèles de conception.

En note de bas de page, entre les mains de sociopathes dérangés, cela peut ne pas bien fonctionner. Cependant, lorsqu'ils sont confrontés à des utilisateurs ou administrateurs sociopathes dérangés, il est préférable de ne pas leur donner Python interprété en premier lieu. Entre les mains du vraiment pervers, Python peut être un handicap; evaln'augmente pas du tout le risque.


7
@Owen S. Le point est le suivant. Les gens vous diront qu'il evals'agit d'une sorte de «vulnérabilité de sécurité». Comme si Python - lui-même - n'était pas simplement un ensemble de sources interprétées que n'importe qui pouvait modifier. Lorsqu'on est confronté à «l'évaluation est une faille de sécurité», vous ne pouvez que supposer que c'est une faille de sécurité entre les mains des sociopathes. Les programmeurs ordinaires modifient simplement la source Python existante et causent directement leurs problèmes. Pas indirectement par evalmagie.
S.Lott

14
Eh bien, je peux vous dire exactement pourquoi je dirais que eval est une vulnérabilité de sécurité, et cela a à voir avec la fiabilité de la chaîne qu'il est donnée en entrée. Si cette chaîne vient, en tout ou en partie, du monde extérieur, il y a une possibilité d'une attaque de script sur votre programme si vous ne faites pas attention. Mais c'est le dérangement d'un attaquant extérieur, pas de l'utilisateur ou de l'administrateur.
Owen S.

6
@OwenS: "Si cette chaîne vient, en tout ou en partie, du monde extérieur" Souvent fausse. Ce n'est pas une chose "prudente". C'est noir et blanc. Si le texte provient d'un utilisateur, il ne peut jamais faire confiance. Le soin n'en fait pas vraiment partie, c'est absolument indigne de confiance. Sinon, le texte provient d'un développeur, d'un installateur ou d'un administrateur et peut être approuvé.
S.Lott

8
@OwenS: Il n'y a pas d'échappatoire possible pour une chaîne de code Python non fiable qui la rendrait fiable. Je suis d'accord avec la plupart de ce que vous dites, à l'exception de la partie «prudente». C'est une distinction très nette. Le code du monde extérieur n'est pas fiable. AFAIK, aucune quantité de fuite ou de filtrage ne peut le nettoyer. Si vous avez une sorte de fonction d'échappement qui rendrait le code acceptable, veuillez partager. Je ne pensais pas qu'une telle chose était possible. Par exemple, il while True: passserait difficile de nettoyer avec une sorte de fuite.
S.Lott

2
@OwenS: "conçu comme une chaîne, pas comme un code arbitraire". Cela n'a aucun rapport. C'est juste une valeur de chaîne, que vous ne passeriez jamais eval(), car c'est une chaîne. Le code du "monde extérieur" ne peut pas être nettoyé. Les chaînes du monde extérieur ne sont que des chaînes. Je ne sais pas de quoi vous parlez. Vous devriez peut-être fournir un article de blog plus complet et un lien vers celui-ci ici.
S.Lott

23

Dans ce cas, oui. Au lieu de

exec 'self.Foo=val'

vous devez utiliser la fonction intégréesetattr :

setattr(self, 'Foo', val)

16

Oui, ça l'est:

Pirater en utilisant Python:

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

Le code ci-dessous répertorie toutes les tâches exécutées sur une machine Windows.

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

Sous Linux:

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

7

Il convient de noter que pour le problème spécifique en question, il existe plusieurs alternatives à l'utilisation eval:

Le plus simple, comme indiqué, consiste à utiliser setattr:

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

Une approche moins évidente consiste à mettre à jour __dict__directement l'objet de l' objet. Si tout ce que vous voulez faire est d'initialiser les attributs sur None, c'est moins simple que ce qui précède. Mais considérez ceci:

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

Cela vous permet de transmettre des arguments de mot-clé au constructeur, par exemple:

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

Cela vous permet également de rendre votre utilisation locals()plus explicite, par exemple:

s = Song(**locals())

... et, si vous voulez vraiment attribuer Noneles attributs dont les noms se trouvent dans locals():

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

Une autre approche pour fournir un objet avec des valeurs par défaut pour une liste d'attributs consiste à définir la __getattr__méthode de la classe :

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

Cette méthode est appelée lorsque l'attribut nommé n'est pas trouvé de la manière normale. Cette approche est un peu moins simple que de simplement définir les attributs dans le constructeur ou de mettre à jour le __dict__, mais elle a le mérite de ne pas créer réellement l'attribut à moins qu'il n'existe, ce qui peut réduire considérablement l'utilisation de la mémoire de la classe.

Le point de tout cela: il y a beaucoup de raisons, en général, à éviter eval- le problème de sécurité de l'exécution de code que vous ne contrôlez pas, le problème pratique du code que vous ne pouvez pas déboguer, etc. Mais une raison encore plus importante est-ce que généralement, vous n'avez pas besoin de l'utiliser. Python expose tellement de ses mécanismes internes au programmeur que vous avez rarement vraiment besoin d'écrire du code qui écrit du code.


1
Une autre façon qui est sans doute plus (ou moins) pythonique: au lieu d'utiliser __dict__directement l'objet, donnez à l'objet un objet dictionnaire réel, soit par héritage, soit comme attribut.
Josh Lee

1
"Une approche moins évidente consiste à mettre à jour directement l'objet dict de l' objet" => Notez que cela contournera tout descripteur (propriété ou autre) ou le __setattr__remplacement, ce qui pourrait conduire à des résultats inattendus. setattr()n'a pas ce problème.
bruno desthuilliers

5

D'autres utilisateurs ont souligné comment votre code peut être modifié pour ne pas dépendre de eval; Je vais proposer un cas d'utilisation légitime eval, celui que l'on trouve même dans CPython: testing .

Voici un exemple que j'ai trouvé dans test_unary.pylequel un test pour savoir si (+|-|~)b'a'lève un TypeError:

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

L'usage n'est clairement pas une mauvaise pratique ici; vous définissez l'entrée et observez simplement le comportement. evalest pratique pour les tests.

Jetez un oeil à cette recherche pour eval, effectuée sur le dépôt git CPython; les tests avec eval sont largement utilisés.


2

Quand eval()est utilisé pour traiter l'entrée fournie par l'utilisateur, vous autorisez l'utilisateur à Drop-to-REPL en fournissant quelque chose comme ceci:

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

Vous pouvez vous en tirer, mais normalement vous ne voulez pas de vecteurs pour l'exécution de code arbitraire dans vos applications.


1

En plus de la réponse @Nadia Alramli, puisque je suis nouveau sur Python et que je voulais vérifier comment l'utilisation evalaffectera les horaires , j'ai essayé un petit programme et ci-dessous se trouvaient les observations:

#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
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.