评估一个string中的mathexpression式
stringExp = "2^4" intVal = int(stringExp) # Expected value: 16
这将返回以下错误:
Traceback (most recent call last): File "<stdin>", line 1, in <module> ValueError: invalid literal for int() with base 10: '2^4'
我知道eval
可以解决这个问题,但是如何评估一个正在存储在一个string中的mathexpression式并不是一个更好,更重要的更安全的方法?
Pyparsing可以用来parsingmathexpression式。 特别是, fourFn.py展示了如何parsing基本的算术expression式。 下面,我已经将fourFn重新包装为一个数字parsing器类,以便于重用。
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
你可以像这样使用它
nsp = NumericStringParser() result = nsp.eval('2^4') print(result) # 16.0 result = nsp.eval('exp(2^4)') print(result) # 8886110.520507872
eval
是邪恶的
eval("__import__('os').remove('important file')") # arbitrary commands eval("9**9**9**9**9**9**9**9", {'__builtins__': None}) # CPU, memory
注意:即使你将set __builtins__
设置为None
,仍然有可能使用自省:
eval('(1).__class__.__bases__[0].__subclasses__()', {'__builtins__': None})
使用ast
评估算术expression式
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> eg, -1 return operators[type(node.op)](eval_(node.operand)) else: raise TypeError(node)
您可以轻松地限制每个操作或任何中间结果的允许范围,例如限制a**b
input参数:
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
或限制中间结果的幅度:
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:
eval()
和sympy.sympify().evalf()
一些更安全的替代方法sympy.sympify().evalf()
* :
- asteval
- numexpr
* SymPy sympify
也是不安全的根据文档中的以下警告。
警告:请注意,此函数使用
eval
,因此不应用于unsanitizedinput。
好的,所以eval的问题在于它可以很容易地逃离它的沙箱,即使你抛弃了__builtins__
。 所有逃避沙箱的方法归结为使用getattr
或object.__getattribute__
(通过.
运算符)通过一些允许的对象( ''.__class__.__bases__[0].__subclasses__
或类似的)获得对某个危险对象的引用。 通过将__builtins__
设置为None
来消除getattr
。 object.__getattribute__
是困难的,因为它不能简单的被删除,因为object
是不可变的,因为删除它会破坏一切。 但是, __getattribute__
只能通过.
运营商,所以从您的input清除足以确保eval不能逃脱其沙箱。
在处理公式时,小数的唯一有效用法是前面或后面有[0-9]
,所以我们只删除所有其他的实例.
。
import re inp = re.sub(r"\.(?![0-9])","", inp) val = eval(inp, {'__builtins__':None})
请注意,虽然python通常将1 + 1.
视为1 + 1.0
,这将删除尾随.
并留下1 + 1
。 你可以添加)
, 和
EOF
列表中允许遵循的事情.
,但是为什么呢?
eval
和exec
的原因非常危险,默认的compile
函数会为任何有效的pythonexpression式生成字节码,并且默认的eval
或者exec
会执行任何有效的python字节码。 所有迄今为止的答案都集中在限制可以生成的字节码(通过消毒input)或使用AST构build自己的域特定语言。
相反,您可以轻松地创build一个简单的eval
函数,该函数无法做任何恶意的事情,并且可以轻松地对运行时间进行内存检查或使用时间。 当然,如果它是简单的math,比有一个捷径。
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]
这个工作的方式很简单,任何常数的mathexpression式在编译过程中被安全的评估并且被存储为一个常量。 编译器返回的代码对象包括d
,它是LOAD_CONST
的字节码,后面是要加载的常量(通常是列表中的最后一个)的编号,后面是S
,这是RETURN_VALUE
的字节码。 如果这个快捷方式不起作用,这意味着用户input不是一个常量expression式(包含一个variables或函数调用或类似的)。
这也打开了一些更复杂的input格式的大门。 例如:
stringExp = "1 + cos(2)"
这需要实际评估字节码,这仍然非常简单。 Python字节码是一种面向堆栈的语言,所以一切都是简单的TOS=stack.pop(); op(TOS); stack.put(TOS)
TOS=stack.pop(); op(TOS); stack.put(TOS)
TOS=stack.pop(); op(TOS); stack.put(TOS)
或类似的。 关键是只实现安全的操作码(加载/存储值,math运算,返回值)而不是不安全的操作(属性查找)。 如果你希望用户能够调用函数(整个原因不使用上面的快捷方式),简单的让你的CALL_FUNCTION
实现只允许在“安全”列表中的函数。
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'))
显然,真正的版本会更长一点(有119个操作码,其中有24个是math相关的)。 添加STORE_FAST
和其他一些可以允许input像'x=5;return x+x
或类似,轻而易举。 它甚至可以用来执行用户创build的函数,只要用户创build的函数是通过VMeval自己执行的(不要使它们可调用!!!或者它们可以用作callback的地方)。 处理循环需要支持goto
字节码,这意味着从迭代器变为while
和保持指向当前指令的指针,但不是太难。 为了抵抗DOS,主循环应该检查自计算开始以来已经过了多less时间,并且某些运算符应该在某个合理的限制( BINARY_POWER
是最明显的)上拒绝input。
虽然这种方法比简单expression式的简单语法分析器稍长一些(参见上面关于刚刚获取编译常量的内容),但它很容易扩展到更复杂的input,并且不需要处理语法( compile
任意复杂的任何东西并减less它到一系列简单的指令)。
您可以使用ast模块并编写一个NodeVisitor来validation每个节点的types是否是白名单的一部分。
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)
因为它通过白名单,而不是黑名单,它是安全的。 它可以访问的唯一函数和variables是您明确赋予其访问权限的variables。 我填充了与math相关的函数的一个字典,以便您可以轻松地提供对这些的访问权限,但是您必须明确地使用它。
如果string尝试调用尚未提供的函数或调用任何方法,则会引发exception,并且不会执行。
因为它使用Python内置的parsing器和评估器,所以它也inheritance了Python的优先级和升级规则。
>>> evaluate("7 + 9 * (2 << 2)") 79 >>> evaluate("6 // 2 + 0.0") 3.0
上面的代码只在Python 3上testing过。
如果需要的话,你可以在这个函数上添加一个超时装饰器。
这是一个非常迟的答复,但我认为对未来的参考很有用。 而不是编写自己的mathparsing器(尽pipe上面的pyparsing示例很好),您可以使用SymPy。 我没有太多的经验,但它包含了比任何人都可能为特定应用程序编写的function更强大的math引擎,基本的expression式评估非常简单:
>>> import sympy >>> x, y, z = sympy.symbols('xy z') >>> sympy.sympify("x**3 + sin(y)").evalf(subs={x:1, y:-3}) 0.858879991940133
确实非常酷! from sympy import *
带来了更多的function支持,比如trig函数,特殊函数等,但是我在这里避免了显示来自哪里的内容。
我想我会使用eval()
,但首先检查,以确保该string是一个有效的mathexpression式,而不是恶意的东西。 你可以使用正则expression式进行validation。
eval()
也可以使用额外的参数来限制它所使用的命名空间,以提高安全性。
[我知道这是一个古老的问题,但值得指出新的有用的解决scheme,因为他们popup]
自从python3.6以来,这个function现在被embedded到语言中 ,创造了“f-string” 。
请参阅: PEP 498 – string插值
例如(注意f
前缀):
f'{2**4}' => '16'
如果你不想使用eval,那么唯一的解决scheme是实现适当的语法分析器。 看看pyparsing 。
在干净的命名空间中使用eval
:
>>> ns = {'__builtins__': None} >>> eval('2 ** 4', ns) 16
干净的命名空间应该防止注入。 例如:
>>> 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__'
否则,你会得到:
>>> eval('__builtins__.__import__("os").system("echo got through")') got through 0
您可能想要访问math模块:
>>> import math >>> ns = vars(math).copy() >>> ns['__builtins__'] = None >>> eval('cos(pi/3)', ns) 0.50000000000000011
Python已经有一个函数来安全地计算包含文字expression式的string:
如果你已经使用了wolframalpha,他们有一个python api,它允许你评估expression式。 可能有点慢,但至less非常准确。