Por que usar 'eval' é uma má prática?

138

Estou usando a classe a seguir para armazenar facilmente dados das minhas músicas.

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

Eu sinto que isso é muito mais extensível do que escrever um if/elsebloco. No entanto, evalparece ser considerado uma prática ruim e insegura de usar. Nesse caso, alguém pode me explicar o porquê e me mostrar uma maneira melhor de definir a classe acima?

Nikwin
fonte
40
como você aprendeu exec/evale ainda não sabia setattr?
U0b34a0f6ae 02/12/2009
3
Acredito que foi a partir de um artigo comparando python e lisp do que aprendi sobre eval.
Nikwin 02/12/2009

Respostas:

194

Sim, usar eval é uma prática ruim. Apenas para citar alguns motivos:

  1. Quase sempre há uma maneira melhor de fazer isso
  2. Muito perigoso e inseguro
  3. Torna a depuração difícil
  4. Lento

No seu caso, você pode usar o 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)

EDITAR:

Existem alguns casos em que você precisa usar eval ou exec. Mas eles são raros. Usar eval no seu caso é uma prática ruim, com certeza. Estou enfatizando as más práticas, porque eval e exec são frequentemente usados ​​no lugar errado.

EDIT 2:

Parece que alguns discordam de que avaliação é 'muito perigosa e insegura' no caso do OP. Isso pode ser verdade para este caso específico, mas não em geral. A pergunta era geral e as razões que listei também são verdadeiras para o caso geral.

EDIT 3: Reordenados pontos 1 e 4

Nadia Alramli
fonte
23
-1: "Muito perigoso e inseguro" é falso. Os outros três são extremamente claros. Reordene-os para que 2 e 4 sejam os dois primeiros. Só é inseguro se você estiver cercado por sociopatas do mal que estão procurando maneiras de subverter sua inscrição.
315 S.Lott
51
@ S.Lott, a insegurança é uma razão muito importante para evitar a avaliação / execução em geral. Muitos aplicativos, como sites, devem ter cuidado extra. Veja o exemplo do OP em um site que espera que os usuários insiram o nome da música. Ele deve ser explorado mais cedo ou mais tarde. Mesmo uma entrada inocente como: Vamos nos divertir. causará um erro de sintaxe e expõe a vulnerabilidade.
Nadia Alramli 02/12/2009
18
@Nadia Alramli: Entrada do usuário e evalnão tem nada a ver um com o outro. Um aplicativo que é fundamentalmente mal projetado é fundamentalmente mal projetado. evalnão é mais a causa raiz do mau design do que a divisão por zero ou a tentativa de importar um módulo que se sabe não existir. evalnão é inseguro. Os aplicativos são inseguros.
315 S.Lott
17
@jeffjose: Na verdade, é fundamentalmente ruim / mau, porque trata dados não-parametrizados como código (é por isso que existem esmagamentos de XSS, injeção de SQL e pilha). @ S.Lott: "Só é inseguro se você estiver cercado por sociopatas do mal que estão procurando maneiras de subverter sua inscrição." Legal, digamos que você crie um programa calce, para adicionar números, ele executa print(eval("{} + {}".format(n1, n2)))e sai. Agora você distribui este programa com algum sistema operacional. Alguém cria um script bash que pega alguns números de um site de ações e os adiciona usandocalc . estrondo?
21
57
Não sei por que a afirmação de Nadia é tão controversa. Parece simples para mim: eval é um vetor para injeção de código e é perigoso de uma maneira que a maioria das outras funções do Python não é. Isso não significa que você não deva usá-lo, mas acho que você deve usá-lo criteriosamente.
Owen S.
32

O uso evalé fraco, não é uma prática claramente ruim .

  1. Ele viola o "Princípio Fundamental do Software". Sua fonte não é a soma total do que é executável. Além da sua fonte, existem argumentos para eval, que devem ser claramente entendidos. Por esse motivo, é a ferramenta de último recurso.

  2. Geralmente é um sinal de design impensado. Raramente existe uma boa razão para o código-fonte dinâmico, criado on-the-fly. Quase tudo pode ser feito com delegação e outras técnicas de design de OO.

  3. Isso leva a uma compilação on-the-fly relativamente lenta de pequenos pedaços de código. Uma sobrecarga que pode ser evitada usando melhores padrões de design.

Como nota de rodapé, nas mãos de sociopatas enlouquecidos, pode não funcionar bem. No entanto, quando confrontados com usuários ou administradores sociopatas enlouquecidos, é melhor não dar a eles o Python interpretado em primeiro lugar. Nas mãos do verdadeiro mal, Python pode ser um passivo; evalnão aumenta o risco.

S.Lott
fonte
7
@ Owen S. O ponto é este. O pessoal dirá que isso evalé algum tipo de "vulnerabilidade de segurança". Como se o Python - por si só - não fosse apenas um monte de fontes interpretadas que alguém pudesse modificar. Quando confrontado com o "eval é uma falha de segurança", você pode apenas assumir que é uma falha de segurança nas mãos dos sociopatas. Programadores comuns apenas modificam a fonte Python existente e causam seus problemas diretamente. Não indiretamente através da evalmagia.
S.Lott
14
Bem, posso dizer exatamente por que diria que eval é uma vulnerabilidade de segurança e tem a ver com a confiabilidade da string que é fornecida como entrada. Se essa sequência vier, no todo ou em parte, do mundo exterior, existe a possibilidade de um ataque de script no seu programa, se você não tomar cuidado. Mas essa é a perturbação de um invasor externo, não do usuário ou administrador.
Owen S.
6
@ OwenS .: "Se essa string vier, no todo ou em parte, do mundo exterior" Muitas vezes falsa. Isso não é uma coisa "cuidadosa". É preto e branco. Se o texto vier de um usuário, ele nunca poderá será confiável. Cuidados não fazem parte disso, são absolutamente não confiáveis. Caso contrário, o texto é de um desenvolvedor, instalador ou administrador e pode ser confiável.
S.Lott
8
@ OwenS .: Não há como escapar de uma sequência de códigos Python não confiáveis ​​que a tornariam confiável. Eu concordo com a maior parte do que você está dizendo, exceto na parte "cuidadosa". É uma distinção muito nítida. Código do mundo exterior não é confiável. AFAIK, nenhuma quantidade de escape ou filtragem pode limpá-lo. Se você tem algum tipo de função de escape que tornaria o código aceitável, compartilhe. Não achei que isso fosse possível. Por exemplo, while True: passseria difícil limpar com algum tipo de fuga.
precisa saber é
2
@OwenS .: "pretende ser uma string, não um código arbitrário". Isso não tem relação. Esse é apenas um valor de string, pelo qual você nunca passaria eval(), pois é uma string. Código do "mundo exterior" não pode ser higienizado. Cordas do mundo exterior são apenas cordas. Não sei ao certo o que você está falando. Talvez você deva fornecer uma postagem de blog mais completa e link para ela aqui.
S.Lott
23

Nesse caso sim. Ao invés de

exec 'self.Foo=val'

você deve usar o builtin função setattr:

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

Sim, ele é:

Corte usando Python:

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

O código abaixo listará todas as tarefas em execução em uma máquina Windows.

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

No Linux:

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

Vale ressaltar que, para o problema específico em questão, existem várias alternativas ao uso eval :

O mais simples, como observado, está usando setattr:

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

Uma abordagem menos óbvia é atualizar o objeto do __dict__objeto diretamente. Se tudo o que você deseja fazer é inicializar os atributos None, isso é menos direto do que o descrito acima. Mas considere isso:

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

Isso permite que você passe argumentos de palavras-chave para o construtor, por exemplo:

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

Também permite que você use locals()mais explícito, por exemplo:

s = Song(**locals())

... e, se você realmente deseja atribuir Noneaos atributos cujos nomes são encontrados em locals():

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

Outra abordagem para fornecer um objeto com valores padrão para uma lista de atributos é definir o __getattr__método da classe :

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

Este método é chamado quando o atributo nomeado não é encontrado da maneira normal. Essa abordagem é um pouco menos direta do que simplesmente definir os atributos no construtor ou atualizar o__dict__ , mas tem o mérito de não criar o atributo, a menos que ele exista, o que pode reduzir substancialmente o uso de memória da classe.

O ponto de tudo isso: em geral, existem muitas razões para evitar eval- o problema de segurança da execução de código que você não controla, o problema prático de código que você não pode depurar etc. etc. Mas uma razão ainda mais importante é que geralmente você não precisa usá-lo. O Python expõe tanto de seus mecanismos internos ao programador que você raramente precisa escrever um código que escreva código.

Robert Rossney
fonte
1
Outra maneira que é sem dúvida mais (ou menos) Pythonic: em vez de usar __dict__diretamente o objeto, forneça ao objeto um objeto de dicionário real, por herança ou como um atributo.
21139 Josh Lee
1
"Uma abordagem menos óbvia é atualizar diretamente o objeto de ditado do objeto" => Observe que isso ignorará qualquer descritor (propriedade ou outra) ou __setattr__substituição, o que pode levar a resultados inesperados. setattr()não tem esse problema.
bruno desthuilliers
5

Outros usuários apontaram como seu código pode ser alterado para não depender eval; Vou oferecer um caso de uso legítimo para uso eval, encontrado mesmo no CPython: testing .

Aqui está um exemplo que encontrei em test_unary.pyque um teste para determinar se (+|-|~)b'a'gera um TypeError:

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

O uso claramente não é uma má prática aqui; você define a entrada e apenas observa o comportamento. evalé útil para teste.

Dê uma olhada esta pesquisa para eval, realizada no repositório git CPython; o teste com eval é muito usado.

Dimitris Fasarakis Hilliard
fonte
2

Quando eval()é usado para processar a entrada fornecida pelo usuário, você habilita o usuário a soltar para o REPL fornecendo algo como isto:

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

Você pode se safar, mas normalmente não deseja vetores para execução arbitrária de código em seus aplicativos.

moooeeeep
fonte
1

Além da resposta da @Nadia Alramli, como eu sou novo no Python e estava ansioso para verificar como o uso evalafetaria os horários , tentei um pequeno programa e abaixo as observações:

#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
Alfaiate de Lokeshwar
fonte