Mengapa menggunakan 'eval' adalah praktik yang buruk?

138

Saya menggunakan kelas berikut untuk menyimpan data lagu saya dengan mudah.

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

Saya merasa ini jauh lebih bisa diperluas daripada menulis satu if/elseblok. Namun, evaltampaknya dianggap praktik yang buruk dan tidak aman untuk digunakan. Jika demikian, adakah yang bisa menjelaskan kepada saya mengapa dan menunjukkan kepada saya cara yang lebih baik untuk mendefinisikan kelas di atas?

Nikwin
sumber
40
bagaimana Anda belajar exec/evaldan masih belum tahu setattr?
u0b34a0f6ae
3
Saya percaya itu dari artikel yang membandingkan python dan lisp daripada yang saya pelajari tentang eval.
Nikwin

Jawaban:

194

Ya, menggunakan eval adalah praktik yang buruk. Hanya untuk menyebutkan beberapa alasan:

  1. Hampir selalu ada cara yang lebih baik untuk melakukannya
  2. Sangat berbahaya dan tidak aman
  3. Membuat proses debug sulit
  4. Lambat

Dalam kasus Anda, Anda dapat menggunakan setattr sebagai gantinya:

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)

EDIT:

Ada beberapa kasus di mana Anda harus menggunakan eval atau exec. Tetapi mereka jarang. Menggunakan eval dalam kasus Anda tentu merupakan praktik yang buruk. Saya menekankan pada praktik buruk karena eval dan eksekutif sering digunakan di tempat yang salah.

EDIT 2:

Sepertinya beberapa orang tidak setuju bahwa eval 'sangat berbahaya dan tidak aman' dalam kasus OP. Itu mungkin benar untuk kasus khusus ini tetapi tidak secara umum. Pertanyaannya adalah umum dan alasan yang saya cantumkan juga benar untuk kasus umum.

EDIT 3: Menyusun ulang poin 1 dan 4

Nadia Alramli
sumber
23
-1: "Sangat berbahaya dan tidak aman" salah. Tiga lainnya sangat jelas. Susun ulang sehingga 2 dan 4 adalah dua yang pertama. Hanya tidak aman jika Anda dikelilingi oleh sosiopat jahat yang mencari cara untuk menumbangkan aplikasi Anda.
S.Lott
51
@ S.Lott, Ketidakamanan adalah alasan yang sangat penting untuk menghindari eval / exec secara umum. Banyak aplikasi seperti situs web harus ekstra hati-hati. Ambil contoh OP di situs web yang mengharapkan pengguna memasukkan nama lagu. Itu pasti akan dieksploitasi cepat atau lambat. Bahkan masukan yang tidak bersalah seperti: Mari bersenang-senang. akan menyebabkan kesalahan sintaks dan mengekspos kerentanan.
Nadia Alramli
18
@Nadia Alramli: Input pengguna dan evaltidak ada hubungannya satu sama lain. Aplikasi yang secara fundamental salah desain pada dasarnya salah desain. evaltidak lebih merupakan akar penyebab desain yang buruk daripada pembagian dengan nol atau berusaha mengimpor modul yang diketahui tidak ada. evaltidak aman. Aplikasi tidak aman.
S.Lott
17
@jeffjose: Sebenarnya, ini pada dasarnya buruk / jahat karena itu memperlakukan data yang tidak dinamikasikan sebagai kode (inilah sebabnya XSS, injeksi SQL, dan tumpukan smash ada). @ S.Lott: "Tidak aman jika Anda dikelilingi oleh sosiopat jahat yang mencari cara untuk menumbangkan aplikasi Anda." Keren, jadi katakan Anda membuat program calc, dan untuk menambahkan angka itu dijalankan print(eval("{} + {}".format(n1, n2)))dan keluar. Sekarang Anda mendistribusikan program ini dengan beberapa OS. Kemudian seseorang membuat skrip bash yang mengambil beberapa angka dari situs stok dan menambahkannya menggunakan calc. ledakan?
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
57
Saya tidak yakin mengapa pernyataan Nadia begitu kontroversial. Tampaknya sederhana bagi saya: eval adalah vektor untuk injeksi kode, dan berbahaya dengan cara yang sebagian besar fungsi Python lainnya tidak. Itu tidak berarti Anda tidak boleh menggunakannya sama sekali, tetapi saya pikir Anda harus menggunakannya dengan bijaksana.
Owen S.
32

Menggunakan evalitu lemah, bukan praktik yang jelas buruk .

  1. Itu melanggar "Prinsip Dasar Perangkat Lunak". Sumber Anda bukan jumlah total dari apa yang dapat dieksekusi. Selain sumber Anda, ada argumen untuk eval, yang harus dipahami dengan jelas. Untuk alasan ini, ini adalah alat pilihan terakhir.

  2. Ini biasanya merupakan tanda desain yang tidak dipikirkan. Jarang ada alasan bagus untuk kode sumber dinamis, dibuat dengan cepat. Hampir semua hal dapat dilakukan dengan pendelegasian dan teknik desain OO lainnya.

  3. Ini menyebabkan kompilasi on-the-fly yang relatif lambat dari potongan-potongan kecil kode. Overhead yang dapat dihindari dengan menggunakan pola desain yang lebih baik.

Sebagai catatan kaki, di tangan para sosiopat gila, itu mungkin tidak berhasil dengan baik. Namun, ketika berhadapan dengan pengguna atau administrator sosiopat yang kacau, yang terbaik adalah tidak memberi mereka Python yang ditafsirkan. Di tangan orang yang benar-benar jahat, Python dapat menjadi tanggung jawab; evalsama sekali tidak meningkatkan risiko.

S.Lott
sumber
7
@ Owen S. Intinya adalah ini. Orang-orang akan memberi tahu Anda bahwa evalada semacam "kerentanan keamanan". Seolah-olah Python - itu sendiri - bukan hanya sekelompok sumber ditafsirkan bahwa siapa pun dapat memodifikasi Ketika dihadapkan dengan "eval is a security hole", Anda hanya bisa berasumsi bahwa itu adalah lubang keamanan di tangan sosiopat. Pemrogram biasa hanya memodifikasi sumber Python yang ada dan menyebabkan masalah mereka secara langsung. Tidak secara tidak langsung melalui evalsihir.
S.Lott
14
Yah, saya bisa memberi tahu Anda dengan tepat mengapa saya akan mengatakan eval adalah kerentanan keamanan, dan itu ada hubungannya dengan keterandalan string yang diberikan sebagai input. Jika string itu datang, seluruhnya atau sebagian, dari dunia luar, ada kemungkinan serangan scripting pada program Anda jika Anda tidak hati-hati. Tapi itu adalah kekacauan penyerang luar, bukan pengguna atau administrator.
Owen S.
6
@WenS .: "Jika string itu datang, seluruhnya atau sebagian, dari dunia luar" Seringkali salah. Ini bukan hal yang "hati-hati". Hitam dan putih. Jika teks berasal dari pengguna, itu tidak akan pernah bisa dipercaya. Perawatan tidak benar-benar bagian dari itu, itu benar-benar tidak dapat dipercaya. Jika tidak, teks berasal dari pengembang, penginstal, atau admin, dan dapat dipercaya.
S.Lott
8
@WenS .: Tidak ada kemungkinan melarikan diri untuk serangkaian kode Python yang tidak dipercaya yang akan membuatnya dipercaya. Saya setuju dengan sebagian besar dari apa yang Anda katakan kecuali untuk bagian "hati-hati". Ini perbedaan yang sangat tajam. Kode dari dunia luar tidak bisa dipercaya. AFAIK, tidak ada jumlah yang lolos atau filter yang bisa membersihkannya. Jika Anda memiliki semacam fungsi pelarian yang membuat kode dapat diterima, silakan bagikan. Saya tidak berpikir hal seperti itu mungkin terjadi. Misalnya while True: passakan sulit untuk dibersihkan dengan semacam pelarian.
S.Lott
2
@WenS .: "dimaksudkan sebagai string, bukan kode arbitrer". Itu tidak berhubungan. Itu hanya nilai string, yang tidak akan pernah Anda lewati eval(), karena itu adalah string. Kode dari "dunia luar" tidak dapat disanitasi. String dari dunia luar hanyalah string. Saya tidak jelas tentang apa yang Anda bicarakan. Mungkin Anda harus memberikan posting blog yang lebih lengkap dan menautkannya di sini.
S.Lott
23

Dalam hal ini, ya. Dari pada

exec 'self.Foo=val'

Anda harus menggunakan fungsi builtinsetattr :

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

Ya itu:

Retas menggunakan Python:

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

Kode di bawah ini akan mencantumkan semua tugas yang berjalan pada mesin Windows.

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

Di Linux:

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

Perlu dicatat bahwa untuk masalah spesifik yang dimaksud, ada beberapa alternatif untuk menggunakan eval:

Yang paling sederhana, seperti disebutkan, menggunakan setattr:

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

Pendekatan yang kurang jelas adalah memperbarui objek __dict__objek secara langsung. Jika semua yang ingin Anda lakukan adalah menginisialisasi atribut None, maka ini kurang mudah daripada di atas. Tetapi pertimbangkan ini:

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

Ini memungkinkan Anda meneruskan argumen kata kunci ke konstruktor, misalnya:

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

Ini juga memungkinkan Anda untuk membuat penggunaan Anda locals()lebih eksplisit, misalnya:

s = Song(**locals())

... dan, jika Anda benar-benar ingin menetapkan Noneatribut yang namanya ditemukan di locals():

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

Pendekatan lain untuk menyediakan objek dengan nilai default untuk daftar atribut adalah dengan mendefinisikan metode kelas __getattr__:

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

Metode ini dipanggil ketika atribut bernama tidak ditemukan dengan cara normal. Pendekatan ini agak kurang langsung daripada hanya mengatur atribut dalam konstruktor atau memperbarui __dict__, tetapi memiliki manfaat tidak benar-benar membuat atribut kecuali jika ada, yang dapat secara substansial mengurangi penggunaan memori kelas.

Inti dari semua ini: Ada banyak alasan, secara umum, untuk dihindari eval- masalah keamanan mengeksekusi kode yang tidak Anda kontrol, masalah praktis kode yang tidak dapat Anda debug, dll. Tapi alasan yang lebih penting adalah bahwa secara umum, Anda tidak perlu menggunakannya. Python memaparkan begitu banyak mekanisme internalnya kepada programmer sehingga Anda jarang benar-benar perlu menulis kode yang menulis kode.

Robert Rossney
sumber
1
Cara lain yang bisa dibilang lebih (atau kurang) Pythonic: Alih-alih menggunakan objek __dict__secara langsung, berikan objek objek kamus yang sebenarnya, baik melalui warisan atau sebagai atribut.
Josh Lee
1
"Pendekatan yang kurang jelas adalah memperbarui objek dict objek secara langsung" => Perhatikan bahwa ini akan mem-bypass setiap deskriptor (properti atau lainnya) atau __setattr__menimpa, yang mungkin mengarah pada hasil yang tidak terduga. setattr()tidak memiliki masalah ini.
bruno desthuilliers
5

Pengguna lain menunjukkan bagaimana kode Anda dapat diubah agar tidak bergantung eval; Saya akan menawarkan use-case yang sah untuk digunakan eval, yang bahkan ditemukan di CPython: testing .

Inilah satu contoh yang saya temukan di test_unary.pymana tes tentang apakah (+|-|~)b'a'menimbulkan TypeError:

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

Penggunaannya jelas bukan praktik yang buruk di sini; Anda mendefinisikan input dan hanya mengamati perilaku. evalberguna untuk pengujian.

Lihatlah pencarian ini untuk eval, dilakukan pada CPython git repositori; pengujian dengan eval banyak digunakan.

Dimitris Fasarakis Hilliard
sumber
2

Saat eval()digunakan untuk memproses input yang disediakan pengguna, Anda memungkinkan pengguna untuk Drop-to-REPL memberikan sesuatu seperti ini:

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

Anda mungkin lolos begitu saja, tetapi biasanya Anda tidak ingin vektor untuk eksekusi kode arbitrer dalam aplikasi Anda.

moooeeeep
sumber
1

Selain jawaban @Nadia Alramli, karena saya baru mengenal Python dan sangat ingin memeriksa bagaimana penggunaan evalakan memengaruhi timing , saya mencoba sebuah program kecil dan di bawah ini adalah pengamatannya:

#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
Penjahit Lokeshwar
sumber