Perché usare 'eval' è una cattiva pratica?


138

Sto usando la seguente classe per memorizzare facilmente i dati delle mie canzoni.

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

Sento che questo è solo molto più estensibile che scrivere un if/elseblocco. Tuttavia, evalsembra essere considerata una cattiva pratica e non sicura da usare. Se è così, qualcuno può spiegarmi perché e mostrarmi un modo migliore per definire la classe di cui sopra?


40
come hai exec/evalsaputo e ancora non lo sapevi setattr?
u0b34a0f6ae,

3
Credo che sia da un articolo che confronta pitone e lisp di quanto ho imparato su eval.
Nikwin

Risposte:


194

Sì, usare eval è una cattiva pratica. Solo per citare alcuni motivi:

  1. C'è quasi sempre un modo migliore per farlo
  2. Molto pericoloso e insicuro
  3. Rende difficile il debug
  4. Lento

Nel tuo caso puoi usare invece setattr :

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)

MODIFICARE:

Ci sono alcuni casi in cui devi usare eval o exec. Ma sono rari. L'uso di eval nel tuo caso è sicuramente una cattiva pratica. Sto sottolineando la cattiva pratica perché eval ed exec sono spesso usati nel posto sbagliato.

EDIT 2:

Sembra che non siano d'accordo sul fatto che eval sia "molto pericoloso e insicuro" nel caso OP. Questo potrebbe essere vero per questo caso specifico, ma non in generale. La domanda era generale e le ragioni che ho elencato sono vere anche per il caso generale.

EDIT 3: riordinati punti 1 e 4


23
-1: "Molto pericoloso e insicuro" è falso. Gli altri tre sono straordinariamente chiari. Si prega di riordinarli in modo che 2 e 4 siano i primi due. È insicuro solo se sei circondato da sociopatici malvagi che sono alla ricerca di modi per sovvertire l'applicazione.
S.Lott

51
@ S.Lott, l'insicurezza è un motivo molto importante per evitare eval / exec in generale. Molte applicazioni come i siti Web dovrebbero prestare particolare attenzione. Prendi l'esempio OP in un sito Web che prevede che gli utenti inseriscano il nome del brano. È destinato ad essere sfruttato prima o poi. Anche un input innocente come: Divertiamoci. causerà un errore di sintassi ed esporrà la vulnerabilità.
Nadia Alramli,

18
@Nadia Alramli: input dell'utente e evalnon hanno nulla a che fare l'uno con l'altro. Un'applicazione che è fondamentalmente mal progettata è fondamentalmente mal progettata. evalnon è più la causa principale della cattiva progettazione della divisione per zero o del tentativo di importare un modulo che non è noto. evalnon è insicuro. Le applicazioni non sono sicure.
S.Lott

17
@jeffjose: In realtà, è fondamentalmente cattivo / cattivo perché tratta i dati non parametrizzati come codice (ecco perché esistono XSS, iniezione SQL e smash dello stack). @ S.Lott: "È insicuro solo se si è circondati da sociopatici malvagi che sono alla ricerca di modi per sovvertire l'applicazione." Bene, quindi supponi di creare un programma calce di aggiungere numeri che esegue print(eval("{} + {}".format(n1, n2)))ed esce. Ora distribuisci questo programma con alcuni sistemi operativi. Quindi qualcuno crea uno script bash che prende alcuni numeri da un sito stock e li aggiunge usando calc. boom?
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

57
Non sono sicuro del perché l'affermazione di Nadia sia così controversa. Mi sembra semplice: eval è un vettore per l'iniezione di codice ed è pericoloso in un modo in cui la maggior parte delle altre funzioni di Python non lo sono. Ciò non significa che non dovresti usarlo affatto, ma penso che dovresti usarlo con giudizio.
Owen S.

32

L'uso evalè debole, non è una pratica chiaramente negativa .

  1. Violi il "Principio fondamentale del software". La tua fonte non è la somma totale di ciò che è eseguibile. Oltre alla tua fonte, ci sono gli argomenti a eval, che devono essere chiaramente compresi. Per questo motivo, è lo strumento di ultima istanza.

  2. Di solito è un segno di design sconsiderato. Raramente c'è una buona ragione per il codice sorgente dinamico, costruito al volo. Quasi tutto può essere fatto con la delega e altre tecniche di progettazione OO.

  3. Porta a una compilazione al volo relativamente lenta di piccoli pezzi di codice. Un sovraccarico che può essere evitato utilizzando modelli di progettazione migliori.

Come nota a piè di pagina, nelle mani di sociopatici squilibrati, potrebbe non funzionare bene. Tuttavia, quando si confrontano con utenti o amministratori sociopatici squilibrati, è meglio non dare loro interpretato Python in primo luogo. Nelle mani del vero male, Python può essere una responsabilità; evalnon aumenta affatto il rischio.


7
@Owen S. Il punto è questo. La gente ti dirà che evalè una sorta di "vulnerabilità della sicurezza". Come se Python - di per sé - non fosse solo un gruppo di fonti interpretate che chiunque poteva modificare. Di fronte al "eval è un buco nella sicurezza", puoi solo supporre che sia un buco nella sicurezza nelle mani dei sociopatici. I programmatori ordinari modificano semplicemente la sorgente Python esistente e causano direttamente i loro problemi. Non indirettamente attraverso la evalmagia.
S.Lott

14
Bene, posso dirti esattamente perché direi che eval è una vulnerabilità di sicurezza e ha a che fare con l'affidabilità della stringa che viene fornita come input. Se quella stringa proviene, in tutto o in parte, dal mondo esterno, c'è la possibilità di un attacco di scripting sul tuo programma se non stai attento. Ma questo è lo squilibrio di un utente malintenzionato esterno, non dell'utente o dell'amministratore.
Owen S.

6
@OwenS .: "Se quella stringa proviene, in tutto o in parte, dal mondo esterno" Spesso falso. Questa non è una cosa "attenta". È in bianco e nero Se il testo proviene da un utente, non può mai essere attendibile. La cura non ne fa davvero parte, è assolutamente inaffidabile. Altrimenti, il testo proviene da uno sviluppatore, installatore o amministratore e può essere considerato attendibile.
S.Lott

8
@OwenS .: Non è possibile scappare per una stringa di codice Python non attendibile che lo renderebbe affidabile. Sono d'accordo con la maggior parte di ciò che stai dicendo, tranne per la parte "attenta". È una distinzione molto nitida. Il codice proveniente dal mondo esterno non è affidabile. AFAIK, nessuna quantità di escape o filtro può ripulirlo. Se hai qualche tipo di funzione di escape che renderebbe il codice accettabile, ti preghiamo di condividere. Non pensavo che una cosa del genere fosse possibile. Ad esempio, while True: passsarebbe difficile ripulire con una sorta di fuga.
S.Lott

2
@OwenS .: "inteso come una stringa, non un codice arbitrario". Non è correlato. Questo è solo un valore di stringa, che non passeresti mai eval(), dal momento che è una stringa. Il codice proveniente dal "mondo esterno" non può essere disinfettato. Le stringhe del mondo esterno sono solo stringhe. Non sono chiaro di cosa tu stia parlando. Forse dovresti fornire un post sul blog più completo e collegarti qui.
S.Lott

23

In questo caso, si. Invece di

exec 'self.Foo=val'

si dovrebbe usare il built funzione di setattr:

setattr(self, 'Foo', val)

16

Sì:

Hack usando Python:

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

Il codice seguente elenca tutte le attività in esecuzione su un computer Windows.

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

In Linux:

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

7

Vale la pena notare che per il problema specifico in questione, ci sono diverse alternative all'utilizzo eval:

Il più semplice, come notato, sta usando setattr:

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

Un approccio meno ovvio è l'aggiornamento __dict__diretto dell'oggetto dell'oggetto. Se tutto ciò che vuoi fare è inizializzare gli attributi None, allora questo è meno semplice di quanto sopra. Ma considera questo:

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

Ciò consente di passare argomenti di parole chiave al costruttore, ad esempio:

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

Ti consente anche di rendere il tuo uso locals()più esplicito, ad esempio:

s = Song(**locals())

... e, se vuoi davvero assegnarlo None agli attributi i cui nomi si trovano in locals():

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

Un altro approccio per fornire un oggetto con valori predefiniti per un elenco di attributi è quello di definire la classe __getattr__ metodo :

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

Questo metodo viene chiamato quando l'attributo denominato non viene trovato nel modo normale. Questo approccio è un po 'meno semplice rispetto alla semplice impostazione degli attributi nel costruttore o all'aggiornamento di__dict__ , ma ha il merito di non creare effettivamente l'attributo a meno che non esista, il che può ridurre sostanzialmente l'utilizzo della memoria della classe.

Il punto di tutto ciò: ci sono molte ragioni, in generale, da evitare eval: il problema di sicurezza dell'esecuzione del codice che non si controlla, il problema pratico del codice che non è possibile eseguire il debug, ecc. Ma una ragione ancora più importante è che in genere non è necessario utilizzarlo. Python espone così tanto dei suoi meccanismi interni al programmatore che raramente hai davvero bisogno di scrivere codice che scriva codice.


1
Un altro modo che è probabilmente più (o meno) Pythonic: invece di usare __dict__direttamente l'oggetto, dai all'oggetto un oggetto dizionario reale, tramite ereditarietà o come attributo.
Josh Lee,

1
"Un approccio meno ovvio è l'aggiornamento diretto dell'oggetto dict dell'oggetto" => Notare che questo ignorerà qualsiasi descrittore (proprietà o altro) o __setattr__sovrascriverà, il che potrebbe portare a risultati imprevisti. setattr()non ha questo problema.
bruno desthuilliers,

5

Altri utenti hanno sottolineato come il codice può essere modificato per non dipendere da eval; Offrirò un caso d'uso legittimo da utilizzare eval, che si trova anche in CPython: testing .

Ecco un esempio che ho trovato in test_unary.pycui un test (+|-|~)b'a'sull'aumento di a TypeError:

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

L'uso non è chiaramente una cattiva pratica qui; tu definisci l'input e semplicemente osservi il comportamento. evalè utile per i test.

Date un'occhiata a questa ricerca per eval, eseguita sul repository git CPython; i test con eval sono ampiamente utilizzati.


2

Quando eval()viene utilizzato per elaborare l'input fornito dall'utente, l'utente consente a Drop-to-REPL di fornire qualcosa del genere:

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

Potresti cavartela, ma normalmente non vuoi vettori per l'esecuzione di codice arbitrario nelle tue applicazioni.


1

Oltre alla risposta di @Nadia Alramli, dato che sono nuovo su Python ed ero ansioso di controllare come l'utilizzo evalinfluirà sui tempi , ho provato un piccolo programma e di seguito sono state le osservazioni:

#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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.