【问题标题】:Is my eval implementation completely safe? [duplicate]我的 eval 实现完全安全吗? [复制]
【发布时间】:2014-09-20 18:39:01
【问题描述】:

我有一个代码,我想让用户通过标准输入三个值,每个类型为floatFractionint,所以我不能在输入上应用ast.literal_eval 来获取我想要的结果。

我读到Eval really is dangerous,显示

  • 提供意见的人如何利用eval 的弱点,以及
  • 一些使eval 调用更安全的方法。

我应用了作者的建议并编写了这段代码,替换了默认的eval

import builtins

class DoubleUnderscoreInEval(ValueError):
    pass

def eval(expression, globals={}, locals=None):
    if '__' in expression:
        raise DoubleUnderscoreInEval('Using __ in eval is not allowed for safety reasons.')
    else:
        if '__builtins__' not in globals:
            globals['__builtins__']={}
        return builtins.eval(expression, globals, locals)

通过我的代码使用builtins.eval 是否完全安全?
如果不是,我可以让eval 调用完全安全吗? (鼓励绕过我的限制的字符串)
如果是,如何?如果没有,我可以用什么代替?

我是 Python 的初学者,因此鼓励任何告诉我如何改进我的代码的 cmets。

My whole code in online compiler

【问题讨论】:

标签: python python-3.x eval


【解决方案1】:

不,您的 eval() 实现安全。其他对象也可以访问__builtins__ 映射。

有关示例,请参阅 Ned Batchelder 的 eval is dangerous

例如,以下字符串可能会导致段错误:

s = """
(lambda fc=(
    lambda n: [
        c for c in 
            ().__class__.__bases__[0].__subclasses__() 
            if c.__name__ == n
        ][0]
    ):
    fc("function")(
        fc("code")(
            0,0,0,0,"KABOOM",(),(),(),"","",0,""
        ),{}
    )()
)()
"""

恐怕仅仅禁止双下划线是不够的。一些有进取心的人会找到一种以一种你没有预料到的创造性方式重新组合双下划线的方法,你会发现自己被黑了反正

ast.literal_eval() 方法使用 AST 解析和自定义处理解析树来支持内置类型。您可以通过添加对“安全”可调用对象的支持来做同样的事情:

import ast

def literal_eval_with_callables(node_or_string, safe_callables=None):
    if safe_callables is None:
        safe_callables = {}
    if isinstance(node_or_string, str):
        node_or_string = ast.parse(node_or_string, mode='eval')
    if isinstance(node_or_string, ast.Expression):
        node_or_string = node_or_string.body
    try:
        # Python 3.4 and up
        ast.NameConstant
        const_test = lambda n: isinstance(n, ast.NameConstant)
        const_extract = lambda n: n.value
    except AttributeError:
        # Everything before
        _const_names = {'None': None, 'True': True, 'False': False}
        const_test = lambda n: isinstance(n, ast.Name) and n.id in _const_names
        const_extract = lambda n: _const_names[n.id]
    def _convert(node):
        if isinstance(node, (ast.Str, ast.Bytes)):
            return node.s
        elif isinstance(node, ast.Num):
            return node.n
        elif isinstance(node, ast.Tuple):
            return tuple(map(_convert, node.elts))
        elif isinstance(node, ast.List):
            return list(map(_convert, node.elts))
        elif isinstance(node, ast.Dict):
            return dict((_convert(k), _convert(v)) for k, v
                        in zip(node.keys, node.values))
        elif const_test(node):
            return const_extract(node)
        elif isinstance(node, ast.UnaryOp) and \
             isinstance(node.op, (ast.UAdd, ast.USub)) and \
             isinstance(node.operand, (ast.Num, ast.UnaryOp, ast.BinOp)):
            operand = _convert(node.operand)
            if isinstance(node.op, ast.UAdd):
                return + operand
            else:
                return - operand
        elif isinstance(node, ast.BinOp) and \
             isinstance(node.op, (ast.Add, ast.Sub)) and \
             isinstance(node.right, (ast.Num, ast.UnaryOp, ast.BinOp)) and \
             isinstance(node.right.n, complex) and \
             isinstance(node.left, (ast.Num, ast.UnaryOp, astBinOp)):
            left = _convert(node.left)
            right = _convert(node.right)
            if isinstance(node.op, ast.Add):
                return left + right
            else:
                return left - right
        elif isinstance(node, ast.Call) and \
             isinstance(node.func, ast.Name) and \
             node.func.id in safe_callables:
            return safe_callables[node.func.id](
                *[_convert(n) for n in node.args],
                **{kw.arg: _convert(kw.value) for kw in node.keywords})
        raise ValueError('malformed string')
    return _convert(node_or_string)

上述函数对implementation of ast.literal_eval() 进行了改编,以添加对特定已注册可调用对象的支持。传入字典命名Fraction

>>> import fractions
>>> safe_callables = {'Fraction': fractions.Fraction}
>>> literal_eval_with_callables('Fraction(1, denominator=2)', safe_callables)
Fraction(1, 2)

上述方法白名单,而不是将要处理的内容列入黑名单。允许调用Fraction,并直接控制将调用的对象,例如。

演示:

>>> samples = '''\
... 2, -9, -35
... 4, -13, 3
... -6, 13, 5
... 5, -6, 6
... -2, 5, -3
... 4, 12, 9
... 0.5, 1, 1
... 1, -0.5, -0.5
... 0.25, Fraction(-1, 3), Fraction(1, 9)'''.splitlines()
>>> safe_callables = {'Fraction': fractions.Fraction}
>>> for line in samples:
...     print(literal_eval_with_callables(line, safe_callables))
... 
(2, -9, -35)
(4, -13, 3)
(-6, 13, 5)
(5, -6, 6)
(-2, 5, -3)
(4, 12, 9)
(0.5, 1, 1)
(1, -0.5, -0.5)
(0.25, Fraction(-1, 3), Fraction(1, 9))

以上内容至少应适用于 Python 3.3 及更高版本,可能更早。 Python 2 需要做更多的工作来支持unicode 字符串并且不会中断ast.Bytes

【讨论】:

  • 下面的字符串包含双下划线,所以我的实现会拒绝它。
  • @GingerPlusPlus:也有办法解决这个问题。底线:不要使用eval()
  • 您推荐的解决方案看起来不错,但不幸的是it doesn't workmy code
  • @GingerPlusPlus:我假设是 Python 2,但我从 Python 3 切换到了适配器版本。
猜你喜欢
  • 1970-01-01
  • 2010-10-01
  • 2013-02-03
  • 2016-10-08
  • 2013-03-08
  • 2012-10-24
  • 1970-01-01
  • 2014-07-25
相关资源
最近更新 更多