stringExp = "2^4"
intVal = int(stringExp) # Expected value: 16
Cela renvoie l'erreur suivante:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: invalid literal for int()
with base 10: '2^4'
Je sais que eval
peut contourner ce problème, mais n’existe-t-il pas une méthode plus sûre et plus importante pour évaluer une expression mathématique stockée dans une chaîne?
Pyparsing peut être utilisé pour analyser des expressions mathématiques. En particulier, fourFn.py .__ montre comment analyser des expressions arithmétiques de base. Ci-dessous, j'ai réemballé fourFn dans une classe d'analyseur numérique pour une réutilisation plus facile.
from __future__ import division
from pyparsing import (Literal, CaselessLiteral, Word, Combine, Group, Optional,
ZeroOrMore, Forward, nums, alphas, oneOf)
import math
import operator
__author__ = 'Paul McGuire'
__version__ = '$Revision: 0.0 $'
__date__ = '$Date: 2009-03-20 $'
__source__ = '''http://pyparsing.wikispaces.com/file/view/fourFn.py
http://pyparsing.wikispaces.com/message/view/home/15549426
'''
__note__ = '''
All I've done is rewrap Paul McGuire's fourFn.py as a class, so I can use it
more easily in other places.
'''
class NumericStringParser(object):
'''
Most of this code comes from the fourFn.py pyparsing example
'''
def pushFirst(self, strg, loc, toks):
self.exprStack.append(toks[0])
def pushUMinus(self, strg, loc, toks):
if toks and toks[0] == '-':
self.exprStack.append('unary -')
def __init__(self):
"""
expop :: '^'
multop :: '*' | '/'
addop :: '+' | '-'
integer :: ['+' | '-'] '0'..'9'+
atom :: PI | E | real | fn '(' expr ')' | '(' expr ')'
factor :: atom [ expop factor ]*
term :: factor [ multop factor ]*
expr :: term [ addop term ]*
"""
point = Literal(".")
e = CaselessLiteral("E")
fnumber = Combine(Word("+-" + nums, nums) +
Optional(point + Optional(Word(nums))) +
Optional(e + Word("+-" + nums, nums)))
ident = Word(alphas, alphas + nums + "_$")
plus = Literal("+")
minus = Literal("-")
mult = Literal("*")
div = Literal("/")
lpar = Literal("(").suppress()
rpar = Literal(")").suppress()
addop = plus | minus
multop = mult | div
expop = Literal("^")
pi = CaselessLiteral("PI")
expr = Forward()
atom = ((Optional(oneOf("- +")) +
(ident + lpar + expr + rpar | pi | e | fnumber).setParseAction(self.pushFirst))
| Optional(oneOf("- +")) + Group(lpar + expr + rpar)
).setParseAction(self.pushUMinus)
# by defining exponentiation as "atom [ ^ factor ]..." instead of
# "atom [ ^ atom ]...", we get right-to-left exponents, instead of left-to-right
# that is, 2^3^2 = 2^(3^2), not (2^3)^2.
factor = Forward()
factor << atom + \
ZeroOrMore((expop + factor).setParseAction(self.pushFirst))
term = factor + \
ZeroOrMore((multop + factor).setParseAction(self.pushFirst))
expr << term + \
ZeroOrMore((addop + term).setParseAction(self.pushFirst))
# addop_term = ( addop + term ).setParseAction( self.pushFirst )
# general_term = term + ZeroOrMore( addop_term ) | OneOrMore( addop_term)
# expr << general_term
self.bnf = expr
# map operator symbols to corresponding arithmetic operations
epsilon = 1e-12
self.opn = {"+": operator.add,
"-": operator.sub,
"*": operator.mul,
"/": operator.truediv,
"^": operator.pow}
self.fn = {"sin": math.sin,
"cos": math.cos,
"tan": math.tan,
"exp": math.exp,
"abs": abs,
"trunc": lambda a: int(a),
"round": round,
"sgn": lambda a: abs(a) > epsilon and cmp(a, 0) or 0}
def evaluateStack(self, s):
op = s.pop()
if op == 'unary -':
return -self.evaluateStack(s)
if op in "+-*/^":
op2 = self.evaluateStack(s)
op1 = self.evaluateStack(s)
return self.opn[op](op1, op2)
Elif op == "PI":
return math.pi # 3.1415926535
Elif op == "E":
return math.e # 2.718281828
Elif op in self.fn:
return self.fn[op](self.evaluateStack(s))
Elif op[0].isalpha():
return 0
else:
return float(op)
def eval(self, num_string, parseAll=True):
self.exprStack = []
results = self.bnf.parseString(num_string, parseAll)
val = self.evaluateStack(self.exprStack[:])
return val
Vous pouvez l'utiliser comme ça
nsp = NumericStringParser()
result = nsp.eval('2^4')
print(result)
# 16.0
result = nsp.eval('exp(2^4)')
print(result)
# 8886110.520507872
eval
est diaboliqueeval("__import__('os').remove('important file')") # arbitrary commands
eval("9**9**9**9**9**9**9**9", {'__builtins__': None}) # CPU, memory
Remarque: même si vous utilisez set __builtins__
sur None
, il est toujours possible d'échapper à l'introspection:
eval('(1).__class__.__bases__[0].__subclasses__()', {'__builtins__': None})
ast
import ast
import operator as op
# supported operators
operators = {ast.Add: op.add, ast.Sub: op.sub, ast.Mult: op.mul,
ast.Div: op.truediv, ast.Pow: op.pow, ast.BitXor: op.xor,
ast.USub: op.neg}
def eval_expr(expr):
"""
>>> eval_expr('2^6')
4
>>> eval_expr('2**6')
64
>>> eval_expr('1 + 2*3**(4^5) / (6 + -7)')
-5.0
"""
return eval_(ast.parse(expr, mode='eval').body)
def eval_(node):
if isinstance(node, ast.Num): # <number>
return node.n
Elif isinstance(node, ast.BinOp): # <left> <operator> <right>
return operators[type(node.op)](eval_(node.left), eval_(node.right))
Elif isinstance(node, ast.UnaryOp): # <operator> <operand> e.g., -1
return operators[type(node.op)](eval_(node.operand))
else:
raise TypeError(node)
Vous pouvez facilement limiter la plage autorisée pour chaque opération ou tout résultat intermédiaire, par exemple, pour limiter les arguments d'entrée pour a**b
:
def power(a, b):
if any(abs(n) > 100 for n in [a, b]):
raise ValueError((a,b))
return op.pow(a, b)
operators[ast.Pow] = power
Ou pour limiter la magnitude des résultats intermédiaires:
import functools
def limit(max_=None):
"""Return decorator that limits allowed returned values."""
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
ret = func(*args, **kwargs)
try:
mag = abs(ret)
except TypeError:
pass # not applicable
else:
if mag > max_:
raise ValueError(ret)
return ret
return wrapper
return decorator
eval_ = limit(max_=10**100)(eval_)
>>> evil = "__import__('os').remove('important file')"
>>> eval_expr(evil) #doctest:+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
TypeError:
>>> eval_expr("9**9")
387420489
>>> eval_expr("9**9**9**9**9**9**9**9") #doctest:+IGNORE_EXCEPTION_DETAIL
Traceback (most recent call last):
...
ValueError:
Quelques alternatives plus sûres que eval()
et sympy.sympify().evalf()
*:
*SymPy sympify
est également dangereux, selon l’avertissement suivant de la documentation.
Attention: Notez que cette fonction utilise
eval
et ne devrait donc pas être utilisée sur une entrée non authentifiée.
Le problème avec eval, c’est qu’il peut échapper trop facilement à son bac à sable, même si vous vous débarrassez de __builtins__
. Toutes les méthodes pour sortir du sandbox consistent à utiliser getattr
ou object.__getattribute__
(via l'opérateur .
) pour obtenir une référence à un objet dangereux via un objet autorisé (''.__class__.__bases__[0].__subclasses__
ou similaire). getattr
est éliminé en définissant __builtins__
sur None
. object.__getattribute__
est difficile, car il ne peut pas être simplement supprimé, à la fois parce que object
est immuable et parce que le supprimer casserait tout. Cependant, __getattribute__
n’est accessible que par l’opérateur .
; il est donc suffisant de purger cette information de votre entrée pour éviter à eval de sortir de son sandbox.
Dans le traitement des formules, la seule utilisation valide d’une décimale est qu’elle soit précédée ou suivie de [0-9]
; nous supprimons donc toutes les autres instances de .
.
import re
inp = re.sub(r"\.(?![0-9])","", inp)
val = eval(inp, {'__builtins__':None})
Notez que bien que python traite normalement 1 + 1.
en tant que 1 + 1.0
, cela supprimera le .
final et vous laissera avec 1 + 1
. Vous pouvez ajouter )
, et
EOF
à la liste des choses autorisées à suivre .
, mais pourquoi s'embêter?
La raison pour laquelle eval
et exec
sont si dangereux est que la fonction compile
par défaut générera du bytecode pour toute expression python valide et que la valeur par défaut eval
ou exec
exécutera tout bytecode python valide. À ce jour, toutes les réponses se sont concentrées sur la limitation du code-octet pouvant être généré (en supprimant les entrées) ou sur la construction de votre propre langage spécifique au domaine à l'aide de l'AST.
Au lieu de cela, vous pouvez facilement créer une simple fonction eval
qui est incapable de faire quoi que ce soit de néfaste et qui peut facilement avoir des vérifications d’exécution sur la mémoire ou le temps utilisé. Bien sûr, s'il s'agit d'un calcul simple, 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 de code renvoyé par compile est constitué de d
, qui est le bytecode de LOAD_CONST
, suivi du numéro de la constante à charger (généralement la dernière dans la liste), suivi de S
, qui est le bytecode de RETURN_VALUE
. Si ce raccourci ne fonctionne pas, cela signifie que la saisie de l'utilisateur n'est pas une expression constante (contient une variable, un appel à une fonction ou similaire).
Cela ouvre également la porte à des formats d’entrée plus sophistiqués. Par exemple:
stringExp = "1 + cos(2)"
Cela nécessite en fait d’évaluer le bytecode, qui reste assez simple. Le bytecode Python est un langage orienté pile, donc tout est une simple question de TOS=stack.pop(); op(TOS); stack.put(TOS)
ou similaire. La clé consiste à implémenter uniquement les opcodes sécurisés (chargement/stockage de valeurs, opérations mathématiques, valeurs renvoyées) et non insalubres (recherche d'attributs). Si vous voulez que l’utilisateur puisse appeler des fonctions (toute la raison pour ne pas utiliser le raccourci ci-dessus), il suffit de faire en sorte que votre implémentation de CALL_FUNCTION
n’autorise que les fonctions figurant dans une liste "sans danger".
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 serait un peu plus longue (il y a 119 opcodes, dont 24 sont liés aux mathématiques). Ajouter STORE_FAST
et quelques autres permettrait une entrée comme 'x=5;return x+x
ou similaire, de manière triviale. 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 !!! sinon elles pourraient être utilisées comme un rappel quelque part). La gestion des boucles nécessite la prise en charge des bytecodes goto
, ce qui signifie que vous passez d'un itérateur for
à while
et que vous maintenez un pointeur sur l'instruction en cours, mais ce n'est pas trop difficile. Pour la résistance à la DOS, la boucle principale doit vérifier le temps écoulé depuis le début du calcul et certains opérateurs doivent refuser les entrées dépassant 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 les expressions simples (voir ci-dessus à propos de la saisie de la constante compilée), elle s'étend facilement à une saisie plus complexe et ne nécessite pas de traitement de la grammaire (compile
prend tout ce qui est arbitrairement compliqué et le réduit à une séquence d'instructions simples).
Vous pouvez utiliser le module ast et écrire un NodeVisitor qui vérifie que le type de chaque nœud fait partie d'une liste blanche.
import ast, math
locals = {key: value for (key,value) in vars(math).items() if key[0] != '_'}
locals.update({"abs": abs, "complex": complex, "min": min, "max": max, "pow": pow, "round": round})
class Visitor(ast.NodeVisitor):
def visit(self, node):
if not isinstance(node, self.whitelist):
raise ValueError(node)
return super().visit(node)
whitelist = (ast.Module, ast.Expr, ast.Load, ast.Expression, ast.Add, ast.Sub, ast.UnaryOp, ast.Num, ast.BinOp,
ast.Mult, ast.Div, ast.Pow, ast.BitOr, ast.BitAnd, ast.BitXor, ast.USub, ast.UAdd, ast.FloorDiv, ast.Mod,
ast.LShift, ast.RShift, ast.Invert, ast.Call, ast.Name)
def evaluate(expr, locals = {}):
if any(elem in expr for elem in '\n#') : raise ValueError(expr)
try:
node = ast.parse(expr.strip(), mode='eval')
Visitor().visit(node)
return eval(compile(node, "<string>", "eval"), {'__builtins__': None}, locals)
except Exception: raise ValueError(expr)
Parce que cela fonctionne via une liste blanche plutôt que sur une liste noire, il est sûr. Les seules fonctions et variables auxquelles il peut accéder sont celles auxquelles vous lui donnez explicitement accès. J'ai rempli un dict avec des fonctions liées aux mathématiques afin que vous puissiez facilement y accéder si vous le souhaitez, mais vous devez l'utiliser explicitement.
Si la chaîne tente d'appeler des fonctions qui n'ont pas été fournies ou d'appeler des méthodes, une exception sera levée et elle ne sera pas exécutée.
Comme il utilise l’analyseur et l’évaluateur intégrés de Python, il hérite également des règles de priorité et de promotion de Python.
>>> evaluate("7 + 9 * (2 << 2)")
79
>>> evaluate("6 // 2 + 0.0")
3.0
Le code ci-dessus n'a été testé que sur Python 3.
Si vous le souhaitez, vous pouvez ajouter un décorateur de délai d’exécution pour cette fonction.
C'est une réponse massivement en retard, mais je pense utile pour référence future. Plutôt que d'écrire votre propre analyseur mathématique (bien que l'exemple ci-dessus soit génial), vous pouvez utiliser SymPy. Je n'ai pas beaucoup d'expérience avec cela, mais il contient un moteur de calcul beaucoup plus puissant que quiconque est susceptible d'écrire pour une application spécifique et l'évaluation de l'expression de base est très simple:
>>> import sympy
>>> x, y, z = sympy.symbols('x y z')
>>> sympy.sympify("x**3 + sin(y)").evalf(subs={x:1, y:-3})
0.858879991940133
Très cool en effet! Un from sympy import *
apporte beaucoup plus de fonctionnalités, telles que des fonctions trigonométriques, des fonctions spéciales, etc., mais je l'ai évité ici pour montrer ce qui vient de où.
_ {[Je sais que c'est une vieille question, mais il convient de signaler de nouvelles solutions utiles à mesure qu'elles apparaissent]]
Depuis python3.6, cette capacité est maintenant intégrée dans le langage, inventé "f-strings".
Voir: PEP 498 - Interpolation de chaîne littérale
Par exemple (notez le préfixe f
):
f'{2**4}'
=> '16'
Je pense que j'utiliserais eval()
, mais vérifierais d'abord que la chaîne est une expression mathématique valide, par opposition à quelque chose de malveillant. Vous pouvez utiliser un regex pour la validation.
eval()
accepte également des arguments supplémentaires que vous pouvez utiliser pour restreindre l'espace de noms dans lequel il opère, pour une sécurité accrue.
Si vous ne souhaitez pas utiliser eval, la seule solution consiste à implémenter l'analyseur de grammaire approprié. Regardez pyparsing .
Utilisez eval
dans un espace de noms propre:
>>> ns = {'__builtins__': None}
>>> eval('2 ** 4', ns)
16
L'espace de noms propre devrait empêcher l'injection. Par exemple:
>>> eval('__builtins__.__import__("os").system("echo got through")', ns)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute '__import__'
Sinon, vous obtiendriez:
>>> eval('__builtins__.__import__("os").system("echo got through")')
got through
0
Vous voudrez peut-être donner accès au module mathématique:
>>> import math
>>> ns = vars(math).copy()
>>> ns['__builtins__'] = None
>>> eval('cos(pi/3)', ns)
0.50000000000000011
Si vous utilisez déjà wolframalpha, ils ont une API python qui vous permet d’évaluer des expressions. Peut-être un peu lent, mais au moins très précis.
Voici ma solution au problème sans utiliser eval. Fonctionne avec Python2 et Python3. Cela ne fonctionne pas avec des nombres négatifs.
$ python -m pytest test.py
test.py
from solution import Solutions
class SolutionsTestCase(unittest.TestCase):
def setUp(self):
self.solutions = Solutions()
def test_evaluate(self):
expressions = [
'2+3=5',
'6+4/2*2=10',
'3+2.45/8=3.30625',
'3**3*3/3+3=30',
'2^4=6'
]
results = [x.split('=')[1] for x in expressions]
for e in range(len(expressions)):
if '.' in results[e]:
results[e] = float(results[e])
else:
results[e] = int(results[e])
self.assertEqual(
results[e],
self.solutions.evaluate(expressions[e])
)
solution.py
class Solutions(object):
def evaluate(self, exp):
def format(res):
if '.' in res:
try:
res = float(res)
except ValueError:
pass
else:
try:
res = int(res)
except ValueError:
pass
return res
def splitter(item, op):
mul = item.split(op)
if len(mul) == 2:
for x in ['^', '*', '/', '+', '-']:
if x in mul[0]:
mul = [mul[0].split(x)[1], mul[1]]
if x in mul[1]:
mul = [mul[0], mul[1].split(x)[0]]
Elif len(mul) > 2:
pass
else:
pass
for x in range(len(mul)):
mul[x] = format(mul[x])
return mul
exp = exp.replace(' ', '')
if '=' in exp:
res = exp.split('=')[1]
res = format(res)
exp = exp.replace('=%s' % res, '')
while '^' in exp:
if '^' in exp:
itm = splitter(exp, '^')
res = itm[0] ^ itm[1]
exp = exp.replace('%s^%s' % (str(itm[0]), str(itm[1])), str(res))
while '**' in exp:
if '**' in exp:
itm = splitter(exp, '**')
res = itm[0] ** itm[1]
exp = exp.replace('%s**%s' % (str(itm[0]), str(itm[1])), str(res))
while '/' in exp:
if '/' in exp:
itm = splitter(exp, '/')
res = itm[0] / itm[1]
exp = exp.replace('%s/%s' % (str(itm[0]), str(itm[1])), str(res))
while '*' in exp:
if '*' in exp:
itm = splitter(exp, '*')
res = itm[0] * itm[1]
exp = exp.replace('%s*%s' % (str(itm[0]), str(itm[1])), str(res))
while '+' in exp:
if '+' in exp:
itm = splitter(exp, '+')
res = itm[0] + itm[1]
exp = exp.replace('%s+%s' % (str(itm[0]), str(itm[1])), str(res))
while '-' in exp:
if '-' in exp:
itm = splitter(exp, '-')
res = itm[0] - itm[1]
exp = exp.replace('%s-%s' % (str(itm[0]), str(itm[1])), str(res))
return format(exp)