เหตุใดการใช้ "eval" จึงเป็นการปฏิบัติที่ไม่ดี


141

ฉันใช้คลาสต่อไปนี้เพื่อจัดเก็บข้อมูลเพลงของฉันได้อย่างง่ายดาย

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

ฉันรู้สึกว่าสิ่งนี้สามารถขยายได้มากกว่าการเขียนif/elseบล็อก อย่างไรก็ตามevalดูเหมือนว่าจะถือเป็นการปฏิบัติที่ไม่ดีและไม่ปลอดภัยในการใช้งาน ถ้าเป็นเช่นนั้นใครช่วยอธิบายให้ฉันเข้าใจว่าทำไมและแสดงวิธีที่ดีกว่าในการกำหนดชั้นเรียนข้างต้น


41
วิธีการที่คุณเรียนรู้เกี่ยวกับexec/evalและยังคงไม่ทราบsetattr?
u0b34a0f6ae

3
ฉันเชื่อว่ามันมาจากบทความเปรียบเทียบ python และ lisp มากกว่าที่ฉันเรียนรู้เกี่ยวกับ eval
Nikwin

คำตอบ:


197

ใช่การใช้ eval เป็นการปฏิบัติที่ไม่ดี เพียงเพื่อบอกเหตุผลบางประการ:

  1. แทบจะมีวิธีที่ดีกว่าในการทำ
  2. อันตรายและไม่ปลอดภัยมาก
  3. ทำให้การดีบักทำได้ยาก
  4. ช้า

ในกรณีของคุณคุณสามารถใช้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)

แก้ไข:

มีบางกรณีที่คุณต้องใช้ eval หรือ exec แต่หายาก การใช้ eval ในกรณีของคุณถือเป็นการปฏิบัติที่ไม่ดีอย่างแน่นอน ฉันเน้นย้ำเรื่องการปฏิบัติที่ไม่ดีเนื่องจากการประเมินและผู้บริหารมักใช้ผิดที่

แก้ไข 2:

ดูเหมือนว่ามีบางคนไม่เห็นด้วยที่การประเมินเป็น 'อันตรายและไม่ปลอดภัย' ในกรณี OP นั่นอาจเป็นจริงสำหรับกรณีนี้ แต่ไม่ใช่โดยทั่วไป คำถามเป็นเรื่องทั่วไปและเหตุผลที่ฉันระบุไว้ก็เป็นจริงสำหรับกรณีทั่วไปเช่นกัน

แก้ไข 3: เรียงลำดับ จุดที่ 1 และ 4 ใหม่


23
-1: "อันตรายและไม่ปลอดภัย" เป็นเท็จ อีกสามคนที่โดดเด่นชัดเจน โปรดจัดลำดับใหม่เพื่อให้ 2 และ 4 เป็นสองรายการแรก จะไม่ปลอดภัยก็ต่อเมื่อคุณถูกล้อมรอบไปด้วยนักสังคมวิทยาที่ชั่วร้ายที่กำลังมองหาวิธีที่จะล้มล้างแอปพลิเคชันของคุณ
ล็อต

53
@ S.Lott ความไม่ปลอดภัยเป็นเหตุผลที่สำคัญมากในการหลีกเลี่ยงการประเมิน / ผู้บริหารโดยทั่วไป แอปพลิเคชันจำนวนมากเช่นเว็บไซต์ควรใช้ความระมัดระวังเป็นพิเศษ ใช้ตัวอย่าง OP ในเว็บไซต์ที่คาดว่าผู้ใช้จะต้องป้อนชื่อเพลง จะต้องถูกใช้ประโยชน์ไม่ช้าก็เร็ว แม้แต่ข้อมูลที่ไร้เดียงสาเช่น: มาสนุกกันเถอะ จะทำให้เกิดข้อผิดพลาดทางไวยากรณ์และทำให้เกิดช่องโหว่
Nadia Alramli

18
@Nadia Alramli: การป้อนข้อมูลของผู้ใช้และevalไม่มีอะไรเกี่ยวข้องกัน แอปพลิเคชันที่ออกแบบผิดพลาดโดยพื้นฐานได้รับการออกแบบที่ผิดพลาดโดยพื้นฐาน evalไม่ใช่สาเหตุหลักของการออกแบบที่ไม่ดีไปกว่าการหารด้วยศูนย์หรือพยายามที่จะนำเข้าโมดูลที่ทราบว่าไม่มีอยู่ evalไม่ปลอดภัย แอปพลิเคชันไม่ปลอดภัย
ล็อต

17
@jeffjose: จริงๆแล้วมันเป็นสิ่งที่ไม่ดีโดยพื้นฐาน / ชั่วร้ายเพราะมันถือว่าข้อมูลที่ไม่มีพารามิเตอร์เป็นรหัส (นี่คือสาเหตุที่ XSS, SQL injection และ stack smashes อยู่) @ S.Lott: "มันไม่ปลอดภัยหากคุณถูกล้อมรอบไปด้วยนักสังคมวิทยาที่ชั่วร้ายที่กำลังมองหาวิธีที่จะล้มล้างใบสมัครของคุณ" เจ๋งมากสมมติว่าคุณสร้างโปรแกรมcalcและเพิ่มตัวเลขมันดำเนินการprint(eval("{} + {}".format(n1, n2)))และออก ตอนนี้คุณแจกจ่ายโปรแกรมนี้กับระบบปฏิบัติการบางระบบ calcใครบางคนแล้วทำให้ทุบตีสคริปต์ที่ใช้ตัวเลขบางส่วนจากเว็บไซต์หุ้นและเพิ่มพวกเขาโดยใช้ บูม?
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳

59
ฉันไม่แน่ใจว่าทำไมคำยืนยันของนาเดียจึงเป็นที่ถกเถียงกันมาก ดูเหมือนง่ายสำหรับฉัน: eval เป็นเวกเตอร์สำหรับการแทรกโค้ดและเป็นอันตรายในลักษณะที่ฟังก์ชัน Python อื่น ๆ ส่วนใหญ่ไม่เป็นเช่นนั้น ไม่ได้หมายความว่าคุณไม่ควรใช้เลย แต่ฉันคิดว่าคุณควรใช้อย่างรอบคอบ
Owen S.

34

การใช้งานevalนั้นอ่อนแอไม่ใช่วิธีปฏิบัติที่ไม่ดีอย่างชัดเจน

  1. เป็นการละเมิด "หลักการพื้นฐานของซอฟต์แวร์" แหล่งที่มาของคุณไม่ใช่ผลรวมของสิ่งที่ปฏิบัติการได้ นอกจากแหล่งที่มาของคุณแล้วยังมีข้อโต้แย้งevalที่ต้องเข้าใจอย่างชัดเจน ด้วยเหตุนี้จึงเป็นเครื่องมือของทางเลือกสุดท้าย

  2. โดยปกติจะเป็นสัญญาณของการออกแบบที่ไร้ความคิด ไม่ค่อยมีเหตุผลที่ดีสำหรับซอร์สโค้ดแบบไดนามิกที่สร้างขึ้นได้ทันที เกือบทุกอย่างสามารถทำได้ด้วยการมอบหมายและเทคนิคการออกแบบ OO อื่น ๆ

  3. นำไปสู่การรวบรวมโค้ดชิ้นเล็ก ๆ ในทันทีที่ค่อนข้างช้า ค่าใช้จ่ายที่สามารถหลีกเลี่ยงได้โดยใช้รูปแบบการออกแบบที่ดีกว่า

ในเชิงอรรถในมือของนักสังคมวิทยาที่บ้าคลั่งอาจไม่ได้ผลดี อย่างไรก็ตามเมื่อต้องเผชิญหน้ากับผู้ใช้หรือผู้ดูแลระบบทางสังคมที่ผิดปกติทางที่ดีที่สุดคืออย่าให้พวกเขาตีความ Python ตั้งแต่แรก Python อยู่ในเงื้อมมือของความชั่วร้ายอย่างแท้จริง evalไม่ได้เพิ่มความเสี่ยงเลย


7
@ โอเว่นเอสประเด็นคือเรื่องนี้ ผู้คนจะบอกคุณว่านั่นevalคือ "ช่องโหว่ด้านความปลอดภัย" บางอย่าง ราวกับว่า Python - ตัวมันเอง - ไม่ใช่แค่แหล่งที่มาของการตีความที่ใคร ๆ ก็สามารถแก้ไขได้ เมื่อเผชิญหน้ากับ "eval is a security hole" คุณสามารถสันนิษฐานได้ว่าเป็นช่องโหว่ด้านความปลอดภัยที่อยู่ในมือของนักสังคมวิทยาเท่านั้น โปรแกรมเมอร์ธรรมดาเพียงแค่แก้ไขซอร์ส Python ที่มีอยู่และทำให้เกิดปัญหาโดยตรง ไม่ทางอ้อมผ่านevalเวทมนตร์.
S.Lott

14
ฉันบอกคุณได้อย่างชัดเจนว่าทำไมฉันถึงบอกว่า eval เป็นช่องโหว่ด้านความปลอดภัยและเกี่ยวข้องกับความน่าเชื่อถือของสตริงที่ให้เป็นอินพุต หากสตริงนั้นมาทั้งหมดหรือบางส่วนจากโลกภายนอกมีความเป็นไปได้ที่จะมีการโจมตีสคริปต์ในโปรแกรมของคุณหากคุณไม่ระมัดระวัง แต่นั่นเป็นความผิดปกติของผู้โจมตีภายนอกไม่ใช่ของผู้ใช้หรือผู้ดูแลระบบ
Owen S.

6
@OwenS: "ถ้าสตริงนั้นมาทั้งหมดหรือบางส่วนจากโลกภายนอก" มักจะเป็นเท็จ นี่ไม่ใช่สิ่งที่ "ระวัง" มันเป็นขาวดำ หากข้อความมาจากผู้ใช้ข้อความนั้นจะไม่สามารถเชื่อถือได้ การดูแลไม่ได้เป็นส่วนหนึ่งของมันจริงๆมันไม่น่าไว้วางใจอย่างแน่นอน มิฉะนั้นข้อความจะมาจากผู้พัฒนาโปรแกรมติดตั้งหรือผู้ดูแลระบบและสามารถเชื่อถือได้
ล็อต

8
@OwenS: ไม่มีทางเป็นไปได้สำหรับสตริงของรหัส Python ที่ไม่น่าเชื่อถือซึ่งจะทำให้เชื่อถือได้ ฉันเห็นด้วยกับสิ่งที่คุณพูดเกือบทั้งหมดยกเว้นในส่วนที่ "ระมัดระวัง" มันเป็นความแตกต่างที่ชัดเจนมาก รหัสจากโลกภายนอกไม่น่าไว้วางใจ AFAIK ไม่มีการหลบหนีหรือการกรองที่สามารถทำความสะอาดได้ หากคุณมีฟังก์ชัน Escape บางอย่างที่จะทำให้โค้ดเป็นที่ยอมรับโปรดแชร์ ฉันไม่คิดว่าสิ่งนี้จะเป็นไปได้ ตัวอย่างเช่นwhile True: passจะยากที่จะทำความสะอาดด้วยการหลบหนีบางประเภท
ล็อต

2
@OwenS: "ตั้งใจเป็นสตริงไม่ใช่รหัสโดยพลการ" นั่นไม่เกี่ยวข้องกัน นั่นเป็นเพียงค่าสตริงซึ่งคุณจะไม่ผ่านeval()เนื่องจากเป็นสตริง รหัสจาก "โลกภายนอก" ไม่สามารถฆ่าเชื้อได้ สตริงจากโลกภายนอกเป็นเพียงสตริง ฉันไม่ชัดเจนว่าคุณกำลังพูดถึงอะไร บางทีคุณควรระบุบล็อกโพสต์ที่สมบูรณ์ยิ่งขึ้นและลิงก์ไปที่นี่
ล็อต


17

ใช่แล้ว:

แฮ็คโดยใช้ Python:

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

โค้ดด้านล่างนี้จะแสดงรายการงานทั้งหมดที่ทำงานบนเครื่อง Windows

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

ใน Linux:

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

7

เป็นที่น่าสังเกตว่าสำหรับปัญหาเฉพาะที่เป็นปัญหามีหลายทางเลือกในการใช้eval:

วิธีที่ง่ายที่สุดคือการใช้setattr:

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

แนวทางที่ชัดเจนน้อยกว่าคือการอัปเดตวัตถุของ__dict__วัตถุโดยตรง หากสิ่งที่คุณต้องการทำคือเริ่มต้นแอตทริบิวต์Noneนี่จะตรงไปตรงมาน้อยกว่าที่กล่าวมา แต่พิจารณาสิ่งนี้:

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

สิ่งนี้ช่วยให้คุณสามารถส่งผ่านอาร์กิวเมนต์คำสำคัญไปยังตัวสร้างเช่น:

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

นอกจากนี้ยังช่วยให้คุณใช้ประโยชน์จากlocals()สิ่งที่ชัดเจนยิ่งขึ้นเช่น:

s = Song(**locals())

... และหากคุณต้องการกำหนดNoneให้กับแอตทริบิวต์ที่มีชื่ออยู่ในlocals():

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

อีกวิธีหนึ่งในการจัดเตรียมวัตถุที่มีค่าเริ่มต้นสำหรับรายการแอตทริบิวต์คือการกำหนด__getattr__วิธีการของคลาส:

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

วิธีนี้จะถูกเรียกเมื่อไม่พบแอตทริบิวต์ที่มีชื่อในลักษณะปกติ วิธีนี้ค่อนข้างตรงไปตรงมาน้อยกว่าการตั้งค่าแอตทริบิวต์ในตัวสร้างหรือการอัปเดต__dict__แต่มีข้อดีที่จะไม่สร้างแอตทริบิวต์จริง ๆ เว้นแต่จะมีอยู่ซึ่งสามารถลดการใช้หน่วยความจำของคลาสได้อย่างมาก

ประเด็นทั้งหมดนี้: โดยทั่วไปมีหลายสาเหตุที่ควรหลีกเลี่ยงeval- ปัญหาด้านความปลอดภัยของการรันโค้ดที่คุณไม่ได้ควบคุมปัญหาในทางปฏิบัติของโค้ดที่คุณไม่สามารถดีบักได้ ฯลฯ แต่เหตุผลที่สำคัญยิ่งกว่า โดยทั่วไปคุณไม่จำเป็นต้องใช้ Python เปิดเผยกลไกภายในมากมายให้กับโปรแกรมเมอร์ซึ่งคุณแทบไม่จำเป็นต้องเขียนโค้ดที่เขียนโค้ด


1
อีกวิธีหนึ่งที่มีเนื้อหามากขึ้น (หรือน้อยกว่า) Pythonic: แทนที่จะใช้วัตถุ__dict__โดยตรงให้วัตถุเป็นวัตถุพจนานุกรมจริงไม่ว่าจะโดยการสืบทอดหรือเป็นแอตทริบิวต์
Josh Lee

1
"แนวทางที่ชัดเจนน้อยกว่าคือการอัปเดตวัตถุdictของวัตถุโดยตรง" => โปรดทราบว่าการดำเนินการนี้จะข้ามตัวอธิบาย (คุณสมบัติหรืออื่น ๆ ) หรือการ__setattr__แทนที่ซึ่งอาจนำไปสู่ผลลัพธ์ที่ไม่คาดคิด setattr()ไม่มีปัญหานี้
bruno desthuilliers

5

คนอื่นชี้ให้เห็นว่ารหัสของคุณสามารถเปลี่ยนแปลงเป็นไปได้ขึ้นอยู่กับeval; ฉันจะมีความถูกต้องตามกฎหมายกรณีการใช้งานสำหรับการใช้evalอย่างใดอย่างหนึ่งที่พบได้แม้ใน CPython: การทดสอบ

นี่คือตัวอย่างหนึ่งที่ฉันพบในtest_unary.pyการทดสอบว่า(+|-|~)b'a'เพิ่ม a TypeError:

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

เห็นได้ชัดว่าการใช้งานไม่ได้มีการปฏิบัติที่ไม่ดีที่นี่ คุณกำหนดอินพุตและสังเกตพฤติกรรมเท่านั้น evalมีประโยชน์สำหรับการทดสอบ

ลองดูที่การค้นหานี้สำหรับการevalดำเนินการเกี่ยวกับการเก็บคอมไพล์ CPython; การทดสอบด้วย eval ถูกใช้อย่างมาก


3

เมื่อeval()ถูกใช้เพื่อประมวลผลอินพุตที่ผู้ใช้ให้มาคุณจะเปิดใช้งานผู้ใช้เพื่อDrop-to-REPL โดยให้สิ่งนี้:

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

คุณอาจจะหนีไป แต่โดยปกติคุณไม่ต้องการเวกเตอร์สำหรับการใช้รหัสโดยอำเภอใจในแอปพลิเคชันของคุณ


1

นอกจากคำตอบของ @Nadia Alramli เนื่องจากฉันยังใหม่กับ Python และกระตือรือร้นที่จะตรวจสอบว่าการใช้งานevalจะส่งผลต่อการกำหนดเวลาอย่างไรฉันจึงลองใช้โปรแกรมขนาดเล็กและด้านล่างเป็นข้อสังเกต:

#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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.