O motivo eval
e exec
são tão perigosos é que a compile
função padrão irá gerar bytecode para qualquer expressão Python válida, e o padrão eval
ou exec
irá executar qualquer bytecode Python válido. Todas as respostas até agora se concentraram em restringir o bytecode que pode ser gerado (limpando a entrada) ou construindo sua própria linguagem de domínio específico usando o AST.
Em vez disso, você pode criar facilmente uma eval
função simples que é incapaz de fazer qualquer coisa nefasta e pode facilmente ter verificações de tempo de execução na memória ou no tempo usado. Claro, se for matemática simples, então existe um atalho.
c = compile(stringExp, 'userinput', 'eval')
if c.co_code[0]==b'd' and c.co_code[3]==b'S':
return c.co_consts[ord(c.co_code[1])+ord(c.co_code[2])*256]
A maneira como isso funciona é simples, qualquer expressão matemática constante é avaliada com segurança durante a compilação e armazenada como uma constante. O objeto de código retornado por compile consiste em d
, que é o bytecode para LOAD_CONST
, seguido pelo número da constante a ser carregada (geralmente a última na lista), seguido por S
, que é o bytecode para RETURN_VALUE
. Se este atalho não funcionar, significa que a entrada do usuário não é uma expressão constante (contém uma variável ou chamada de função ou semelhante).
Isso também abre a porta para alguns formatos de entrada mais sofisticados. Por exemplo:
stringExp = "1 + cos(2)"
Isso requer uma avaliação real do bytecode, que ainda é bastante simples. O bytecode Python é uma linguagem orientada a pilha, então tudo é uma questão simples TOS=stack.pop(); op(TOS); stack.put(TOS)
ou semelhante. A chave é implementar apenas os opcodes que são seguros (carregar / armazenar valores, operações matemáticas, valores de retorno) e não os inseguros (pesquisa de atributo). Se você deseja que o usuário seja capaz de chamar funções (toda a razão para não usar o atalho acima), simplesmente faça sua implementação de CALL_FUNCTION
permitir apenas funções em uma lista 'segura'.
from dis import opmap
from Queue import LifoQueue
from math import sin,cos
import operator
globs = {'sin':sin, 'cos':cos}
safe = globs.values()
stack = LifoQueue()
class BINARY(object):
def __init__(self, operator):
self.op=operator
def __call__(self, context):
stack.put(self.op(stack.get(),stack.get()))
class UNARY(object):
def __init__(self, operator):
self.op=operator
def __call__(self, context):
stack.put(self.op(stack.get()))
def CALL_FUNCTION(context, arg):
argc = arg[0]+arg[1]*256
args = [stack.get() for i in range(argc)]
func = stack.get()
if func not in safe:
raise TypeError("Function %r now allowed"%func)
stack.put(func(*args))
def LOAD_CONST(context, arg):
cons = arg[0]+arg[1]*256
stack.put(context['code'].co_consts[cons])
def LOAD_NAME(context, arg):
name_num = arg[0]+arg[1]*256
name = context['code'].co_names[name_num]
if name in context['locals']:
stack.put(context['locals'][name])
else:
stack.put(context['globals'][name])
def RETURN_VALUE(context):
return stack.get()
opfuncs = {
opmap['BINARY_ADD']: BINARY(operator.add),
opmap['UNARY_INVERT']: UNARY(operator.invert),
opmap['CALL_FUNCTION']: CALL_FUNCTION,
opmap['LOAD_CONST']: LOAD_CONST,
opmap['LOAD_NAME']: LOAD_NAME
opmap['RETURN_VALUE']: RETURN_VALUE,
}
def VMeval(c):
context = dict(locals={}, globals=globs, code=c)
bci = iter(c.co_code)
for bytecode in bci:
func = opfuncs[ord(bytecode)]
if func.func_code.co_argcount==1:
ret = func(context)
else:
args = ord(bci.next()), ord(bci.next())
ret = func(context, args)
if ret:
return ret
def evaluate(expr):
return VMeval(compile(expr, 'userinput', 'eval'))
Obviamente, a versão real disso seria um pouco mais longa (há 119 opcodes, 24 dos quais relacionados à matemática). Adicionar STORE_FAST
e alguns outros permitiria uma entrada semelhante 'x=5;return x+x
ou semelhante, com uma facilidade trivial. Ele pode até mesmo ser usado para executar funções criadas pelo usuário, desde que as funções criadas pelo usuário sejam executadas via VMeval (não as torne chamáveis !!! ou elas poderiam ser usadas como um retorno de chamada em algum lugar). O tratamento de loops requer suporte para os goto
bytecodes, o que significa mudar de um for
iterador para while
e manter um ponteiro para a instrução atual, mas não é muito difícil. Para resistência ao DOS, o loop principal deve verificar quanto tempo se passou desde o início do cálculo, e certos operadores devem negar a entrada acima de algum limite razoável (BINARY_POWER
sendo o mais óbvio).
Embora essa abordagem seja um pouco mais longa do que um analisador gramatical simples para expressões simples (veja acima sobre apenas pegar a constante compilada), ela se estende facilmente para entradas mais complicadas e não requer lidar com gramática ( compile
pegue qualquer coisa arbitrariamente complicada e a reduz para uma sequência de instruções simples).