La raison eval
et exec
sont si dangereuses est que la compile
fonction par défaut générera du bytecode pour toute expression python valide, et la valeur par défaut eval
ou exec
exécutera tout bytecode python valide. Toutes les réponses à ce jour se sont concentrées sur la restriction du bytecode qui peut être généré (en nettoyant l'entrée) ou sur la création de votre propre langage spécifique au domaine à l'aide de l'AST.
Au lieu de cela, vous pouvez facilement créer une eval
fonction simple qui est incapable de faire quoi que ce soit de néfaste et qui peut facilement avoir des contrôles d'exécution sur la mémoire ou le temps utilisé. Bien sûr, s'il s'agit de mathématiques simples, il existe un raccourci.
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]
La façon dont cela fonctionne est simple, toute expression mathématique constante est évaluée en toute sécurité lors de la compilation et stockée sous forme de constante. L'objet code retourné par compile se compose de d
, qui est le bytecode pour LOAD_CONST
, suivi du numéro de la constante à charger (généralement la dernière de la liste), suivi de S
, qui est le bytecode pour RETURN_VALUE
. Si ce raccourci ne fonctionne pas, cela signifie que l'entrée utilisateur n'est pas une expression constante (contient un appel de variable ou de fonction ou similaire).
Cela ouvre également la porte à certains formats d'entrée plus sophistiqués. Par exemple:
stringExp = "1 + cos(2)"
Cela nécessite en fait d'évaluer le bytecode, ce qui est encore assez simple. Le bytecode Python est un langage orienté pile, donc tout est simple TOS=stack.pop(); op(TOS); stack.put(TOS)
ou similaire. La clé est de n'implémenter que les opcodes qui sont sûrs (chargement / stockage de valeurs, opérations mathématiques, retour de valeurs) et non dangereux (recherche d'attributs). Si vous voulez que l'utilisateur puisse appeler des fonctions (la raison pour laquelle ne pas utiliser le raccourci ci-dessus), faites simplement votre implémentation de CALL_FUNCTION
n'autoriser que les fonctions dans une liste «sûre».
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'))
De toute évidence, la version réelle de cela serait un peu plus longue (il y a 119 opcodes, dont 24 sont liés aux mathématiques). L'ajout STORE_FAST
et quelques autres permettraient des entrées similaires 'x=5;return x+x
ou similaires, très facilement. Il peut même être utilisé pour exécuter des fonctions créées par l'utilisateur, à condition que les fonctions créées par l'utilisateur soient elles-mêmes exécutées via VMeval (ne les rendez pas appelables !!! ou elles pourraient être utilisées comme un rappel quelque part). La gestion des boucles nécessite la prise en charge des goto
bytecodes, ce qui signifie passer d'un for
itérateur à while
et maintenir un pointeur vers l'instruction courante, mais ce n'est pas trop difficile. Pour la résistance au DOS, la boucle principale doit vérifier le temps écoulé depuis le début du calcul, et certains opérateurs doivent refuser l'entrée au-delà d'une limite raisonnable (BINARY_POWER
étant le plus évident).
Bien que cette approche soit un peu plus longue qu'un simple analyseur de grammaire pour des expressions simples (voir ci-dessus à propos de la capture de la constante compilée), elle s'étend facilement à une entrée plus compliquée, et ne nécessite pas de traiter la grammaire ( compile
prenez quelque chose de compliqué arbitrairement et le réduit à une séquence d'instructions simples).