Dlaczego używanie słowa „eval” jest złą praktyką?

138

Używam następującej klasy, aby łatwo przechowywać dane moich piosenek.

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

Uważam, że jest to o wiele bardziej rozszerzalne niż pisanie if/elsebloku. Jednak evalwydaje się być uważany za złą praktykę i niebezpieczny w użyciu. Jeśli tak, czy ktoś może mi wyjaśnić dlaczego i pokazać lepszy sposób zdefiniowania powyższej klasy?

Nikwin
źródło
40
skąd się dowiedziałeś exec/evali nadal nie wiedziałeś setattr?
u0b34a0f6ae
3
Wydaje mi się, że było to z artykułu porównującego Pythona i seplenienie, niż dowiedziałem się o eval.
Nikwin

Odpowiedzi:

194

Tak, używanie eval to zła praktyka. Żeby wymienić tylko kilka powodów:

  1. Prawie zawsze jest na to lepszy sposób
  2. Bardzo niebezpieczny i niepewny
  3. Utrudnia debugowanie
  4. Powolny

W twoim przypadku możesz zamiast tego użyć 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)

EDYTOWAĆ:

W niektórych przypadkach musisz użyć funkcji eval lub exec. Ale są rzadkie. Używanie eval w twoim przypadku jest z pewnością złą praktyką. Podkreślam złe praktyki, ponieważ eval i exec są często używane w niewłaściwym miejscu.

EDYCJA 2:

Wygląda na to, że niektórzy nie zgadzają się, że eval jest „bardzo niebezpieczny i niepewny” w przypadku OP. Może to być prawdą w tym konkretnym przypadku, ale nie ogólnie. Pytanie było ogólne, a powody, które wymieniłem, są prawdziwe również dla przypadku ogólnego.

EDYCJA 3: Zmieniono kolejność punktów 1 i 4

Nadia Alramli
źródło
22
-1: „Bardzo niebezpieczne i niepewne” jest fałszem. Pozostałe trzy są wyjątkowo jasne. Zmień ich kolejność, tak aby 2 i 4 były pierwszymi dwoma. Jest to niebezpieczne tylko wtedy, gdy jesteś otoczony przez złych socjopatów, którzy szukają sposobów na obalenie twojego wniosku.
S.Lott
51
@ S.Lott, Brak bezpieczeństwa jest bardzo ważnym powodem, dla którego należy unikać eval / exec w ogóle. W przypadku wielu aplikacji, takich jak strony internetowe, należy zachować szczególną ostrożność. Weźmy przykład OP w witrynie internetowej, która oczekuje od użytkowników wprowadzenia nazwy utworu. Prędzej czy później z pewnością zostanie wykorzystany. Nawet niewinny wpis typu: Bawmy się dobrze. spowoduje błąd składni i ujawni lukę.
Nadia Alramli
17
@Nadia Alramli: dane wejściowe użytkownika i evalnie mają ze sobą nic wspólnego. Aplikacja, która jest zasadniczo źle zaprojektowana, jest zasadniczo źle zaprojektowana. evalnie jest bardziej główną przyczyną złego projektu niż dzielenie przez zero lub próba zaimportowania modułu, o którym wiadomo, że nie istnieje. evalnie jest niepewne. Aplikacje są niezabezpieczone.
S.Lott
17
@jeffjose: Właściwie jest zasadniczo zły / zły, ponieważ traktuje niesparamateralizowane dane jako kod (dlatego istnieją XSS, wstrzykiwanie SQL i smugi stosu). @ S.Lott: „To niepewne tylko wtedy, gdy jesteś otoczony przez złych socjopatów, którzy szukają sposobów na obalenie twojego wniosku”. Fajnie, więc powiedz, że tworzysz program calc, a aby dodać liczby, wykonuje go print(eval("{} + {}".format(n1, n2)))i kończy. Teraz rozpowszechniasz ten program z jakimś systemem operacyjnym. Następnie ktoś tworzy skrypt basha, który pobiera kilka liczb z witryny giełdowej i dodaje je za pomocą calc. Bum?
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
57
Nie jestem pewien, dlaczego twierdzenie Nadii jest tak kontrowersyjne. Wydaje mi się to proste: eval jest wektorem do wstrzykiwania kodu i jest niebezpieczny w sposób, w jaki większość innych funkcji Pythona nie jest. Nie oznacza to, że w ogóle nie powinieneś go używać, ale myślę, że powinieneś używać go rozsądnie.
Owen S.,
32

Używanie evaljest słabe i nie jest wyraźnie złą praktyką.

  1. Narusza „Podstawową zasadę oprogramowania”. Twoje źródło nie jest sumą tego, co jest wykonywalne. Oprócz twojego źródła istnieją argumenty eval, które należy jasno zrozumieć. Z tego powodu jest to narzędzie ostatniej szansy.

  2. Zwykle jest to oznaka bezmyślnego projektu. Rzadko istnieje dobry powód dla dynamicznego kodu źródłowego, budowanego w locie. Prawie wszystko można zrobić za pomocą delegacji i innych technik projektowania obiektowego.

  3. Prowadzi to do stosunkowo powolnej kompilacji małych fragmentów kodu w locie. Narzut, którego można uniknąć, stosując lepsze wzorce projektowe.

Jak przypis, w rękach obłąkanych socjopatów może się to nie udać. Jednak w konfrontacji z obłąkanymi socjopatycznymi użytkownikami lub administratorami najlepiej nie dawać im interpretowanego języka Python. W rękach naprawdę złego Python może być ciężarem;evalw ogóle nie zwiększa ryzyka.

S.Lott
źródło
7
@Owen S. Chodzi o to. Ludzie powiedzą ci, że evaljest to rodzaj „luki w zabezpieczeniach”. Jakby sam Python nie był tylko zbiorem interpretowanych źródeł, które każdy mógł zmodyfikować. W konfrontacji z „eval jest luką bezpieczeństwa”, można tylko założyć, że jest to luka w rękach socjopatów. Zwykli programiści po prostu modyfikują istniejące źródła Pythona i bezpośrednio powodują problemy. Nie pośrednio przez evalmagię.
S.Lott
14
Cóż, mogę ci dokładnie powiedzieć, dlaczego powiedziałbym, że eval jest luką w zabezpieczeniach i ma to związek z wiarygodnością ciągu, który jest podawany jako dane wejściowe. Jeśli ten ciąg pochodzi, w całości lub w części, ze świata zewnętrznego, istnieje możliwość ataku skryptowego na twój program, jeśli nie będziesz ostrożny. Ale to jest krzywda osoby atakującej z zewnątrz, a nie użytkownika czy administratora.
Owen S.
6
@ OwenS .: "Jeśli ten ciąg pochodzi, w całości lub w części, ze świata zewnętrznego" Często jest fałszywy. To nie jest „ostrożna” rzecz. To jest czarnobiałe. Jeśli tekst pochodzi od użytkownika, nigdy nie można mu ufać. Opieka tak naprawdę nie jest częścią tego, jest absolutnie nie do zaufania. W przeciwnym razie tekst pochodzi od programisty, instalatora lub administratora i można mu zaufać.
S.Lott,
8
@ OwenS .: Nie ma możliwości ucieczki dla ciągu niezaufanego kodu Pythona, który uczyniłby go godnym zaufania. Zgadzam się z większością tego, co mówisz, z wyjątkiem części „ostrożnej”. To bardzo wyraźne rozróżnienie. Kod ze świata zewnętrznego jest nie do zaufania. AFAIK, żadna ilość ucieczki lub filtrowania nie może go oczyścić. Jeśli masz jakąś funkcję ucieczki, która sprawiłaby, że kod byłby akceptowalny, udostępnij. Nie sądziłem, że coś takiego jest możliwe. Na przykład while True: passtrudno byłoby posprzątać jakimś rodzajem ucieczki.
S.Lott,
2
@ OwenS .: "zamierzone jako ciąg znaków, a nie dowolny kod". To nie ma związku. To tylko wartość ciągu, przez którą nigdy byś nie przeszedł eval(), ponieważ jest to ciąg. Kodu ze „świata zewnętrznego” nie można oczyścić. Struny ze świata zewnętrznego to tylko struny. Nie wiem, o czym mówisz. Być może powinieneś podać bardziej kompletny post na blogu i link do niego tutaj.
S.Lott,
23

W tym przypadku tak. Zamiast

exec 'self.Foo=val'

powinieneś użyć wbudowanej funkcji setattr:

setattr(self, 'Foo', val)
Josh Lee
źródło
16

Tak to jest:

Hack używając Pythona:

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

Poniższy kod zawiera listę wszystkich zadań uruchomionych na komputerze z systemem Windows.

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

W systemie Linux:

>>> eval(input())
"__import__('subprocess').Popen(['ps', 'aux'],stdout=__import__('subprocess').PIPE).communicate()[0]"
Hackaholic
źródło
7

Warto zauważyć, że w przypadku konkretnego problemu istnieje kilka alternatyw do użycia eval:

Najprostszym, jak wspomniano, jest użycie setattr:

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

Mniej oczywistym podejściem jest bezpośrednia aktualizacja obiektu __dict__obiektu. Jeśli wszystko, co chcesz zrobić, to zainicjować atrybuty None, jest to mniej proste niż powyższe. Ale rozważ to:

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

Pozwala to na przekazanie argumentów słów kluczowych do konstruktora, np .:

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

Pozwala także na locals()bardziej jednoznaczne użycie , np .:

s = Song(**locals())

... a jeśli naprawdę chcesz przypisać Nonedo atrybutów, których nazwy znajdują się w locals():

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

Innym podejściem do dostarczenia obiektowi wartości domyślnych dla listy atrybutów jest zdefiniowanie metody klasy __getattr__:

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

Ta metoda jest wywoływana, gdy nazwany atrybut nie zostanie znaleziony w normalny sposób. To podejście jest nieco mniej proste niż po prostu ustawienie atrybutów w konstruktorze lub zaktualizowanie pliku__dict__ , ale ma tę zaletę, że nie tworzy atrybutu, chyba że istnieje, co może znacznie zmniejszyć użycie pamięci klasy.

W tym wszystkim chodzi o to, że istnieje wiele powodów, których należy unikać eval- problem bezpieczeństwa związany z wykonywaniem kodu, którego nie kontrolujesz, praktyczny problem kodu, którego nie możesz debugować itp. Ale jeszcze ważniejszy powód polega na tym, że generalnie nie musisz go używać. Python udostępnia programiście tyle swoich wewnętrznych mechanizmów, że rzadko trzeba pisać kod, który pisze kod.

Robert Rossney
źródło
1
Inny sposób, który jest prawdopodobnie bardziej (lub mniej) Pythonowy: Zamiast używać __dict__bezpośrednio obiektu , nadaj obiektowi rzeczywisty obiekt słownika, albo przez dziedziczenie, albo jako atrybut.
Josh Lee
1
"Mniej oczywistym podejściem jest bezpośrednia aktualizacja obiektu dyktowania obiektu" => Zauważ, że spowoduje to pominięcie dowolnego deskryptora (właściwość lub inny) lub __setattr__zastąpienie, co może prowadzić do nieoczekiwanych wyników. setattr()nie ma tego problemu.
bruno desthuilliers
5

Inni użytkownicy wskazywali, jak można zmienić kod, aby nie polegać na eval; Zaproponuję uzasadniony przypadek użycia eval, który można znaleźć nawet w CPythonie: testing .

Oto jeden przykład, który znalazłem, test_unary.pygdzie test sprawdzający, czy (+|-|~)b'a'podnosi a TypeError:

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

Użycie nie jest tutaj złą praktyką; definiujesz dane wejściowe i po prostu obserwujesz zachowanie. evaljest przydatny do testowania.

Spójrz na tego wyszukiwania dla evalprzeprowadzana jest na git repozytorium CPython; testowanie z eval jest często używane.

Dimitris Fasarakis Hilliard
źródło
2

Gdy eval()jest używany do przetwarzania danych wejściowych wprowadzonych przez użytkownika, umożliwiasz użytkownikowi Drop-to-REPL, podając coś takiego:

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

Może ci się to udać, ale zwykle nie chcesz wektorów do wykonywania dowolnego kodu w swoich aplikacjach.

moooeeeep
źródło
1

Oprócz odpowiedzi @Nadia Alramli, ponieważ jestem nowy w Pythonie i bardzo chciałem sprawdzić, jak użycie evalwpłynie na czasy , wypróbowałem mały program i poniżej były spostrzeżenia:

#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
Lokeshwar Tailor
źródło