Tại sao sử dụng 'eval' là một thực hành xấu?


137

Tôi đang sử dụng lớp sau để dễ dàng lưu trữ dữ liệu các bài hát của mình.

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

Tôi cảm thấy rằng điều này chỉ mở rộng hơn nhiều so với việc viết ra một if/elsekhối. Tuy nhiên, evaldường như được coi là một thực hành xấu và không an toàn để sử dụng. Nếu vậy, bất cứ ai có thể giải thích cho tôi tại sao và chỉ cho tôi một cách tốt hơn để xác định lớp trên?


40
Làm thế nào bạn tìm hiểu về exec/evalvà vẫn không biết setattr?
u0b34a0f6ae

3
Tôi tin rằng đó là từ một bài báo so sánh python và lisp hơn tôi đã học về eval.
Nikwin

Câu trả lời:


194

Vâng, sử dụng eval là một thực hành xấu. Chỉ cần nêu một vài lý do:

  1. Hầu như luôn luôn có một cách tốt hơn để làm điều đó
  2. Rất nguy hiểm và không an toàn
  3. Làm cho việc sửa lỗi trở nên khó khăn
  4. Chậm

Trong trường hợp của bạn, bạn có thể sử dụng setattr thay thế:

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)

BIÊN TẬP:

Có một số trường hợp bạn phải sử dụng eval hoặc exec. Nhưng chúng rất hiếm. Sử dụng eval trong trường hợp của bạn là một thực tế xấu chắc chắn. Tôi nhấn mạnh vào thực tiễn xấu vì eval và exec thường được sử dụng sai chỗ.

EDIT 2:

Có vẻ như một số người không đồng ý rằng eval là 'rất nguy hiểm và không an toàn' trong trường hợp OP. Điều đó có thể đúng với trường hợp cụ thể này nhưng không nói chung. Câu hỏi rất chung chung và những lý do tôi liệt kê cũng đúng cho trường hợp chung.

EDIT 3: Sắp xếp lại điểm 1 và 4


22
-1: "Rất nguy hiểm và không an toàn" là sai. Ba người còn lại rất rõ ràng. Vui lòng sắp xếp lại chúng để 2 và 4 là hai đầu tiên. Nó chỉ không an toàn nếu bạn bị bao vây bởi những kẻ xã hội xấu xa đang tìm cách lật đổ ứng dụng của bạn.
S.Lott

51
@ S.Lott, Bảo mật là một lý do rất quan trọng để tránh eval / exec nói chung. Nhiều ứng dụng như trang web nên cẩn thận hơn. Lấy ví dụ OP trong một trang web mong muốn người dùng nhập tên bài hát. Nó chắc chắn sẽ được khai thác sớm hay muộn. Ngay cả một đầu vào vô tội như: Hãy vui vẻ. sẽ gây ra lỗi cú pháp và lộ lỗ hổng.
Nadia Alramli

17
@Nadia Alramli: Đầu vào của người dùng và evalkhông liên quan gì đến nhau. Một ứng dụng được thiết kế sai về cơ bản được thiết kế sai về cơ bản. evalkhông còn là nguyên nhân sâu xa của thiết kế xấu hơn là chia cho số 0 hoặc cố gắng nhập một mô-đun được biết là không tồn tại. evalkhông phải là không an toàn. Ứng dụng không an toàn.
S.Lott

17
@jeffjose: Trên thực tế, về cơ bản xấu / xấu vì nó coi dữ liệu không được kiểm duyệt là mã (đây là lý do tại sao XSS, SQL tiêm và ngăn xếp smash tồn tại). @ S.Lott: "Chỉ không an toàn nếu bạn bị bao vây bởi những kẻ xã hội xấu xa đang tìm cách lật đổ ứng dụng của bạn." Thật tuyệt, vì vậy, giả sử bạn tạo một chương trình calc, và để thêm số, nó sẽ thực thi print(eval("{} + {}".format(n1, n2)))và thoát. Bây giờ bạn phân phối chương trình này với một số hệ điều hành. Sau đó, một người nào đó tạo ra một tập lệnh bash lấy một số số từ một trang web chứng khoán và thêm chúng bằng cách sử dụng calc. bùng nổ?
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

57
Tôi không chắc tại sao khẳng định của Nadia lại gây tranh cãi như vậy. Nó có vẻ đơn giản đối với tôi: eval là một vectơ để tiêm mã và nguy hiểm theo cách mà hầu hết các hàm Python khác không có. Điều đó không có nghĩa là bạn không nên sử dụng nó, nhưng tôi nghĩ bạn nên sử dụng nó một cách thận trọng.
Owen S.

32

Sử dụng evallà yếu, không phải là một thực hành xấu rõ ràng .

  1. Nó vi phạm "Nguyên tắc cơ bản của phần mềm". Nguồn của bạn không phải là tổng số của những gì có thể thực hiện được. Ngoài nguồn của bạn, còn có các đối số eval, phải được hiểu rõ ràng. Vì lý do này, nó là công cụ cuối cùng.

  2. Nó thường là một dấu hiệu của thiết kế thiếu suy nghĩ. Hiếm khi có một lý do chính đáng cho mã nguồn động, được xây dựng nhanh chóng. Hầu như bất cứ điều gì có thể được thực hiện với ủy quyền và các kỹ thuật thiết kế OO khác.

  3. Nó dẫn đến việc biên dịch tương đối chậm trên các đoạn mã nhỏ. Một chi phí có thể tránh được bằng cách sử dụng các mẫu thiết kế tốt hơn.

Như một chú thích, trong tay của những kẻ xã hội loạn trí, nó có thể không hoạt động tốt. Tuy nhiên, khi phải đối mặt với người dùng hoặc quản trị viên xã hội loạn trí, tốt nhất là không cho họ giải thích Python ngay từ đầu. Trong tay của kẻ ác thực sự, Python có thể phải chịu trách nhiệm; evalkhông làm tăng rủi ro


7
@Owen S. Vấn đề là đây. Mọi người sẽ nói với bạn rằng đó evallà một loại "lỗ hổng bảo mật". Như thể Python - chính nó - không chỉ là một loạt các nguồn được giải thích mà bất cứ ai cũng có thể sửa đổi. Khi đối mặt với "eval là một lỗ hổng bảo mật", bạn chỉ có thể cho rằng đó là một lỗ hổng bảo mật trong tay của những kẻ xã hội. Các lập trình viên thông thường chỉ sửa đổi nguồn Python hiện có và gây ra vấn đề của họ trực tiếp. Không gián tiếp thông qua evalphép thuật.
S.Lott

14
Chà, tôi có thể nói cho bạn biết chính xác lý do tại sao tôi lại nói eval là một lỗ hổng bảo mật và nó phải liên quan đến độ tin cậy của chuỗi mà nó đưa ra làm đầu vào. Nếu chuỗi đó xuất hiện, toàn bộ hoặc một phần, từ thế giới bên ngoài, có khả năng một cuộc tấn công kịch bản vào chương trình của bạn nếu bạn không cẩn thận. Nhưng đó là sự phá hoại của một kẻ tấn công bên ngoài, không phải của người dùng hoặc quản trị viên.
Owen S.

6
@OwenS.: "Nếu chuỗi đó đến, toàn bộ hoặc một phần, từ thế giới bên ngoài" Thường là sai. Đây không phải là một điều "cẩn thận". Nó màu đen và trắng. Nếu văn bản đến từ người dùng, nó không bao giờ có thể tin cậy được. Chăm sóc không thực sự là một phần của nó, nó hoàn toàn không đáng tin cậy. Mặt khác, văn bản đến từ nhà phát triển, trình cài đặt hoặc quản trị viên và có thể được tin cậy.
S.Lott

8
@OwenS.: Không có khả năng thoát cho một chuỗi mã Python không đáng tin cậy sẽ làm cho nó đáng tin cậy. Tôi đồng ý với hầu hết những gì bạn đang nói ngoại trừ phần "cẩn thận". Đó là một sự khác biệt rất rõ nét. Mã từ thế giới bên ngoài là không đáng tin cậy. AFAIK, không có số lượng thoát hoặc lọc có thể làm sạch nó. Nếu bạn có một số loại chức năng thoát sẽ làm cho mã được chấp nhận, xin vui lòng chia sẻ. Tôi đã không nghĩ rằng một điều như vậy là có thể. Ví dụ while True: passsẽ khó dọn dẹp với một số loại thoát.
S.Lott

2
@OwenS.: "Dự định là một chuỗi, không phải mã tùy ý". Điều đó không liên quan. Đó chỉ là một giá trị chuỗi, mà bạn sẽ không bao giờ vượt qua eval(), vì đó là một chuỗi. Mã từ "thế giới bên ngoài" không thể được vệ sinh. Chuỗi từ thế giới bên ngoài chỉ là chuỗi. Tôi không rõ những gì bạn đang nói về. Có lẽ bạn nên cung cấp một bài viết blog đầy đủ hơn và liên kết đến nó ở đây.
S.Lott

23

Trong trường hợp này, có. Thay vì

exec 'self.Foo=val'

bạn nên sử dụng hàm dựng sẵnsetattr :

setattr(self, 'Foo', val)

16

Vâng, đúng vậy:

Hack bằng Python:

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

Đoạn mã dưới đây sẽ liệt kê tất cả các tác vụ đang chạy trên máy Windows.

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

Trong Linux:

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

7

Điều đáng chú ý là đối với vấn đề cụ thể đang được đề cập, có một số lựa chọn thay thế cho việc sử dụng eval :

Đơn giản nhất, như đã lưu ý, là sử dụng setattr:

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

Một cách tiếp cận ít rõ ràng hơn là cập nhật __dict__trực tiếp đối tượng của đối tượng. Nếu tất cả những gì bạn muốn làm là khởi tạo các thuộc tính None, thì điều này ít đơn giản hơn so với ở trên. Nhưng hãy xem xét điều này:

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

Điều này cho phép bạn chuyển các đối số từ khóa cho hàm tạo, ví dụ:

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

Nó cũng cho phép bạn sử dụng locals()rõ ràng hơn, ví dụ:

s = Song(**locals())

... và, nếu bạn thực sự muốn gán Nonecho các thuộc tính có tên được tìm thấy trong locals():

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

Một cách tiếp cận khác để cung cấp một đối tượng với các giá trị mặc định cho danh sách các thuộc tính là xác định __getattr__phương thức của lớp :

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

Phương thức này được gọi khi thuộc tính được đặt tên không được tìm thấy theo cách thông thường. Cách tiếp cận này hơi đơn giản hơn là chỉ đơn giản là đặt các thuộc tính trong hàm tạo hoặc cập nhật__dict__ , nhưng nó có ưu điểm là không thực sự tạo thuộc tính trừ khi nó tồn tại, điều này có thể làm giảm đáng kể việc sử dụng bộ nhớ của lớp.

Vấn đề của tất cả những điều này: nói chung, có rất nhiều lý do để tránh eval- vấn đề bảo mật khi thực thi mã mà bạn không kiểm soát, vấn đề thực tế về mã bạn không thể gỡ lỗi, v.v. Nhưng một lý do thậm chí còn quan trọng hơn nói chung là bạn không cần sử dụng nó. Python tiết lộ rất nhiều cơ chế bên trong của nó cho lập trình viên mà bạn hiếm khi thực sự cần phải viết mã viết mã.


1
Một cách khác được cho là nhiều hơn (hoặc ít hơn) Pythonic: Thay vì sử dụng __dict__trực tiếp đối tượng , hãy cung cấp cho đối tượng một đối tượng từ điển thực tế, thông qua kế thừa hoặc như một thuộc tính.
Josh Lee

1
"Một cách tiếp cận ít rõ ràng hơn là cập nhật trực tiếp đối tượng dict của đối tượng" => Lưu ý rằng điều này sẽ bỏ qua bất kỳ mô tả nào (thuộc tính hoặc khác) hoặc __setattr__ghi đè, điều này có thể dẫn đến kết quả không mong muốn. setattr()không có vấn đề này
bruno Desthuilliers

5

Những người dùng khác chỉ ra cách mã của bạn có thể được thay đổi để không phụ thuộc vào eval; Tôi sẽ cung cấp một trường hợp sử dụng hợp pháp để sử dụng eval, một trường hợp được tìm thấy ngay cả trong CPython: thử nghiệm .

Đây là một ví dụ tôi tìm thấy trong test_unary.pyđó một bài kiểm tra về việc có (+|-|~)b'a'làm tăng TypeError:

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

Việc sử dụng rõ ràng là thực hành không tồi ở đây; bạn xác định đầu vào và chỉ quan sát hành vi. evallà tiện dụng để thử nghiệm.

Hãy nhìn vào tìm kiếm này cho eval, thực hiện trên kho git CPython; thử nghiệm với eval được sử dụng rất nhiều.


2

Khi eval()được sử dụng để xử lý dữ liệu đầu vào do người dùng cung cấp, bạn cho phép người dùng Drop-to-REPL cung cấp một cái gì đó như thế này:

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

Bạn có thể thoát khỏi nó, nhưng thông thường bạn không muốn các vectơ để thực thi mã tùy ý trong các ứng dụng của mình.


1

Ngoài câu trả lời @Nadia Alramli, vì tôi chưa quen với Python và rất muốn kiểm tra việc sử dụng evalsẽ ảnh hưởng đến thời gian như thế nào , tôi đã thử một chương trình nhỏ và dưới đây là các quan sát:

#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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.