Почему использование eval - плохая практика?

138

Я использую следующий класс, чтобы легко хранить данные моих песен.

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кажется, считается плохой практикой и небезопасно использовать. Если так, может кто-нибудь объяснить мне, почему и показать мне лучший способ определения вышеупомянутого класса?

Nikwin
источник
40
как ты узнал exec/evalи до сих пор не знал setattr?
u0b34a0f6ae
3
Я полагаю, что из статьи, сравнивающей python и lisp, я узнал о eval.
Никвин

Ответы:

194

Да, использование 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 в вашем случае наверняка плохая практика. Я подчеркиваю плохую практику, потому что eval и exec часто используются не в том месте.

РЕДАКТИРОВАТЬ 2:

Похоже, что некоторые не согласны с тем, что eval является «очень опасным и небезопасным» в случае с OP. Это может быть верно для этого конкретного случая, но не в целом. Вопрос был общим, и причины, которые я перечислил, верны и для общего случая.

РЕДАКТИРОВАТЬ 3: Изменение порядка пунктов 1 и 4

Надя Алрамли
источник
23
-1: «Очень опасно и небезопасно» - ложь. Остальные три совершенно ясны. Пожалуйста, измените их порядок так, чтобы 2 и 4 были первыми двумя. Это небезопасно, только если вы окружены злыми социопатами, которые ищут способы подорвать ваше приложение.
S.Lott
51
@ S.Lott, Небезопасность - очень важная причина, чтобы избегать eval / exec в целом. Многие приложения, такие как веб-сайты, должны проявлять особую осторожность. Возьмите пример OP на веб-сайте, который ожидает, что пользователи введут название песни. Рано или поздно он будет эксплуатироваться. Даже невинный ввод, как: Давайте веселиться. вызовет синтаксическую ошибку и выставит уязвимость.
Надя Алрамли
18
@ Надя Алрамли: Пользовательский ввод и не evalимеет ничего общего друг с другом. Приложение, которое в корне неправильно спроектировано, в корне неправильно спроектировано. evalявляется не более чем основной причиной плохого дизайна, чем деление на ноль или попытка импортировать модуль, который, как известно, не существует. evalне небезопасно Приложения небезопасны.
S.Lott
17
@jeffjose: На самом деле, это в основном плохо / зло, потому что он рассматривает непараметризованные данные как код (именно поэтому существуют XSS, SQL-инъекции и разрушения стека). @ S.Lott: «Это небезопасно, только если вы окружены злыми социопатами, которые ищут способы подорвать ваше приложение». Круто, так сказать, вы делаете программу calc, и для добавления чисел она выполняется print(eval("{} + {}".format(n1, n2)))и выходит. Теперь вы распространяете эту программу с некоторыми ОС. Затем кто-то делает скрипт bash, который берет некоторые цифры со стокового сайта и добавляет их, используя calc. бум?
L̲̳o̲̳̳n̲̳̳g̲̳̳p̲̳o̲̳̳k̲̳̳e̲̳̳
57
Я не уверен , почему утверждение Надя настолько спорное. Мне это кажется простым: eval - это вектор для внедрения кода, и он опасен в отличие от большинства других функций Python. Это не значит, что вы не должны использовать это вообще, но я думаю, что вы должны использовать это разумно.
Оуэн С.
32

Использование evalслабое, не совсем плохая практика.

  1. Это нарушает «Фундаментальный принцип программного обеспечения». Ваш источник не является общей суммой исполняемого файла. Помимо вашего источника, есть и аргументы eval, которые должны быть четко поняты. По этой причине это инструмент последней инстанции.

  2. Обычно это признак бездумного замысла. Там редко бывают веские причины для динамического исходного кода, созданного на лету. Почти все может быть сделано с делегированием и другими методами проектирования ОО.

  3. Это приводит к относительно медленной на лету компиляции небольших фрагментов кода. Накладные расходы, которых можно избежать, используя лучшие шаблоны проектирования.

В качестве сноски, в руках ненормальных социопатов, это может не сработать. Однако, когда сталкиваешься с ненормальными социопатическими пользователями или администраторами, лучше не давать им интерпретируемый Python. В руках действительно зла, Python может быть ответственностью; evalне увеличивает риск вообще.

С. Лотт
источник
7
@ Оуэн С. Дело в том. Люди скажут вам, что evalэто какая-то «уязвимость безопасности». Как будто Python - сам по себе - не просто набор интерпретируемого источника, который любой может изменить. Столкнувшись с «eval - это дыра в безопасности», вы можете только предполагать, что это дыра в безопасности в руках социопатов. Обычные программисты просто модифицируют существующий исходный код Python и напрямую вызывают свои проблемы. Не косвенно через evalмагию.
С.Лотт
14
Что ж, я могу точно сказать, почему я бы сказал, что eval - это уязвимость безопасности, и это связано с достоверностью строки, которую она выдает в качестве входных данных. Если эта строка полностью или частично исходит из внешнего мира, есть вероятность атаки с использованием сценариев на вашу программу, если вы не будете осторожны. Но это расстройство внешнего злоумышленника, а не пользователя или администратора.
Оуэн С.
6
@OwenS .: «Если эта строка полностью или частично исходит из внешнего мира» Часто ложно. Это не "осторожная" вещь. Это черно-белое. Если текст исходит от пользователя, он может никогда нельзя доверять. Забота на самом деле не часть этого, это абсолютно ненадежно. В противном случае текст поступает от разработчика, установщика или администратора, и ему можно доверять.
С.Лотт
8
@OwenS .: Невозможно избежать строки ненадежного кода Python, которая сделала бы его надежным. Я согласен с большей частью того, что вы говорите, за исключением «осторожной» части. Это очень четкое различие. Код из внешнего мира не заслуживает доверия. AFAIK, никакое количество экранирования или фильтрации не может очистить его. Если у вас есть какая-то экранирующая функция, которая делает код приемлемым, пожалуйста, поделитесь. Я не думал, что это возможно. Например, while True: passбыло бы трудно убрать с каким-то побегом.
С.Лотт
2
@OwenS .: «предназначен как строка, а не произвольный код». Это не связано. Это просто строковое значение, через которое вы никогда не пройдете eval(), так как это строка. Код из «внешнего мира» не может быть санирован. Струны из внешнего мира - это просто струны. Мне неясно, о чем ты говоришь. Возможно, вы должны предоставить более полный пост в блоге и ссылку на него здесь.
S.Lott
23

В этом случае да. Вместо того

exec 'self.Foo=val'

Вы должны использовать встроенную функцию setattr:

setattr(self, 'Foo', val)
Джош Ли
источник
16

Да, это так:

Взломать с помощью 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]"
Hackaholic
источник
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__непосредственного использования объекта дайте объекту фактический объект словаря, либо через наследование, либо как атрибут.
Джош Ли
1
«Менее очевидный подход заключается в непосредственном обновлении объекта dict объекта» => Обратите внимание, что это обойдет любой дескриптор (свойство или другое) или __setattr__переопределение, что может привести к неожиданным результатам. setattr()не имеет этой проблемы
Bruno Desthuilliers
5

Другие пользователи указали, как ваш код может быть изменен, чтобы не зависеть от eval; Я предложу законный вариант использования eval, который встречается даже в CPython: тестирование .

Вот один пример, который я нашел в том, test_unary.pyгде тест на то, (+|-|~)b'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 активно используется.

Димитрис Фасаракис Хиллиард
источник
2

Когда eval()используется для обработки предоставленного пользователем ввода, вы разрешаете пользователю Drop-to-REPL предоставлять что-то вроде этого:

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

Вам это может сойти с рук, но обычно вам не нужны векторы для выполнения произвольного кода в ваших приложениях.

moooeeeep
источник
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
Локешвар Портной
источник