Python eval:如果禁用内置函数和属性访问,它还是危险的吗?

45

我们都知道eval是危险的,即使你隐藏了危险函数,因为你可以使用Python的内省特性来深入挖掘并重新提取它们。例如,即使您删除__builtins__,您仍然可以使用以下方式检索到它们:

[c for c in ().__class__.__base__.__subclasses__()  
 if c.__name__ == 'catch_warnings'][0]()._module.__builtins__

然而,我看到的所有示例都使用属性访问。如果我禁用所有内置函数,并禁用属性访问(通过使用Python标记解析器对输入进行标记化并在具有属性访问令牌时拒绝它),该怎么办?
在你问之前,不,根据我的用例,我不需要这两个功能,所以这不会太严重。
我试图使SymPy的sympify函数更加安全。目前,它对输入进行标记化,进行一些转换,并在命名空间中评估它。但是它是不安全的,因为它允许属性访问(即使它实际上并不需要它)。

9
这取决于你所说的“危险”是什么意思……我想攻击者可以创建一个表达式来生成一个非常大的整数,导致你的内存耗尽…… - mgilson
3
@mgilson 的观点很有道理。我认为可以通过在应用程序中加入内存/时间保护来防止这种情况发生,但一定要注意。 - asmeurer
9
我认为这也取决于你经过的当地人... a + b 的安全性仅取决于 a.__add__b.__radd__ 的安全性... - mgilson
6
ast.literal_eval可以实现吗?或者你需要更多的内容但仍然不涉及属性?那么函数调用呢? - Jason S
6
相关链接:http://blog.delroth.net/2013/03/escaping-a-python-sandbox-ndh-2013-quals-writeup/ - wim
显示剩余3条评论
6个回答

35

我将提到Python 3.6的新特性之一 - f-strings

它们可以评估表达式,

>>> eval('f"{().__class__.__base__}"', {'__builtins__': None}, {})
"<class 'object'>"

但是 Python 的词法分析器不会检测到属性访问:

0,0-0,0:            ENCODING       'utf-8'        
1,0-1,1:            ERRORTOKEN     "'"            
1,1-1,27:           STRING         'f"{().__class__.__base__}"'
2,0-2,0:            ENDMARKER      '' 

1
你只需要考虑所有f-strings的内容并检查它们(或更安全地:禁止它们)。 - Bakuriu
31
这真正凸显了保护eval的难度有多大。现在,涉及到f-strings。谁知道3.7会带来什么呢? - user2357112
1
虽然它在stdlib的tokenize模块中并不显示,但f-string内部的表达式将在解析f"{some code}"时显示在AST中。 - Arminius
1
f-string 中的表达式不必包含任何属性访问节点,即可进行属性访问 - 例如 f"{eval('()' + chr(46) + '__class__')}" - kaya3

22

使用eval构建返回值是可能会在尝试printlogrepr或其他任何操作时,在eval之外抛出一个异常的。

eval('''((lambda f: (lambda x: x(x))(lambda y: f(lambda *args: y(y)(*args))))
        (lambda f: lambda n: (1,(1,(1,(1,f(n-1))))) if n else 1)(300))''')

这将创建一个嵌套的元组,形式为(1,(1,(1,(1...;该值无法在Python 3中进行printstrrepr操作;所有尝试调试它的尝试都会导致...
RuntimeError: maximum recursion depth exceeded while getting the repr of a tuple

pprintsaferepr也失败了:

...
  File "/usr/lib/python3.4/pprint.py", line 390, in _safe_repr
    orepr, oreadable, orecur = _safe_repr(o, context, maxlevels, level)
  File "/usr/lib/python3.4/pprint.py", line 340, in _safe_repr
    if issubclass(typ, dict) and r is dict.__repr__:
RuntimeError: maximum recursion depth exceeded while calling a Python object

因此,没有安全的内置函数可以将其字符串化:以下助手可能会有用:
def excsafe_repr(obj):
    try:
        return repr(obj)
    except:
        return object.__repr__(obj).replace('>', ' [exception raised]>')

还有一个问题,Python 2中的print实际上并不使用str/repr,因此由于缺乏递归检查,您没有任何安全保障。也就是说,如果您取上面lambda函数的返回值,则无法strrepr它,但普通的print(而不是print_function!)可以很好地打印它。然而,如果您知道将使用print语句打印它,则可以利用这一点在Python 2上生成SIGSEGV。

print eval('(lambda i: [i for i in ((i, 1) for j in range(1000000))][-1])(1)')
Python 2崩溃并出现SIGSEGV错误在错误跟踪器中,这被标记为不修复。因此,如果想要安全,请勿使用print语句。请使用from __future__ import print_function
这不是崩溃,而是
eval('(1,' * 100 + ')' * 100)

运行时输出

s_push: parser stack overflow
Traceback (most recent call last):
  File "yyy.py", line 1, in <module>
    eval('(1,' * 100 + ')' * 100)
MemoryError
MemoryError 可以被捕获,是 Exception 的子类。解析器有一些 非常保守的限制,以避免由于堆栈溢出而导致崩溃(双关语)。但是,s_push:parser stack overflow 由 C 代码输出到 stderr,无法被抑制。

我昨天刚问过为什么Python 3.4不能修复崩溃问题,


% python3  
Python 3.4.3 (default, Mar 26 2015, 22:03:40) 
[GCC 4.9.2] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> class A:
...     def f(self):
...         nonlocal __x
... 
[4]    19173 segmentation fault (core dumped)  python3

Serhiy Storchaka的回答证实Python核心开发人员不认为在看似格式良好的代码上发生SIGSEGV是安全问题:

只有针对3.4的安全修复才会被接受。

因此,可以得出结论:无论是否经过消毒处理,从第三方执行任何代码都永远不能被认为是安全的。

然后Nick Coghlan 补充道

关于为什么Python代码引发的分段错误目前不被视为安全漏洞的一些背景信息:由于CPython不包括安全沙盒,我们已经完全依赖操作系统提供进程隔离。 操作系统级别的安全边界不受代码是否以“正常”方式运行或在故意触发分段错误后以修改状态运行的影响。


因此,没有安全的方法将此值转储到日志或其他任何地方 - 任何尝试都会导致抛出进一步的异常。这是否值得修复? - cat
3
看,Haskell没有那个问题:-D 即使是最奇怪的东西,要么会崩溃并且可以轻松捕获,要么会转化为普通的无限长字符串,你可以打印任意长的一部分。 - John Dvorak
第一个可以使用 f-strings 在 3.6 版本中实现,无需 eval。 - cat
@AnttiHaapala 接受的答案表明这正是f-strings的目的。 - cat
顺便说一下,nonlocal __x 的问题在3.6.0a0中已经修复,尽管这个修复已经有2个月了,但在打包(apt)的3.5版本中仍然存在。 - cat
显示剩余2条评论

15

用户仍然可以通过输入一个评估为巨大数字的表达式来对您进行拒绝服务攻击,这会填满您的内存并崩溃Python进程,例如

'10**10**100'

我仍然很好奇是否可能发动更传统的攻击,例如恢复内置函数或创建一个段错误。

编辑:

事实证明,即使是Python的解析器也存在这个问题。

lambda: 10**10**100

会挂起,因为它试图预计算常量。


避免这种情况的唯一方法是使用超时,阻止正在运行的线程在x时间或执行太多分配后(这可能非常困难...) - Bakuriu
1
@Bakuriu:如果你在Python中工作,那么这将会更加困难,因为这很可能在持有GIL的同时进行评估。对于如此大的数字,根据情况,也存在OOM的风险。 - Kevin

9

以下是一个 safe_eval 的示例,它将确保所评估的表达式不包含不安全的令牌。它并不尝试采用解释 AST 的字面量评估方法,而是将令牌类型加入白名单,并在通过测试后使用真正的评估。

# license: MIT (C) tardyp
import ast


def safe_eval(expr, variables):
    """
    Safely evaluate a a string containing a Python
    expression.  The string or node provided may only consist of the following
    Python literal structures: strings, numbers, tuples, lists, dicts, booleans,
    and None. safe operators are allowed (and, or, ==, !=, not, +, -, ^, %, in, is)
    """
    _safe_names = {'None': None, 'True': True, 'False': False}
    _safe_nodes = [
        'Add', 'And', 'BinOp', 'BitAnd', 'BitOr', 'BitXor', 'BoolOp',
        'Compare', 'Dict', 'Eq', 'Expr', 'Expression', 'For',
        'Gt', 'GtE', 'Is', 'In', 'IsNot', 'LShift', 'List',
        'Load', 'Lt', 'LtE', 'Mod', 'Name', 'Not', 'NotEq', 'NotIn',
        'Num', 'Or', 'RShift', 'Set', 'Slice', 'Str', 'Sub',
        'Tuple', 'UAdd', 'USub', 'UnaryOp', 'boolop', 'cmpop',
        'expr', 'expr_context', 'operator', 'slice', 'unaryop']
    node = ast.parse(expr, mode='eval')
    for subnode in ast.walk(node):
        subnode_name = type(subnode).__name__
        if isinstance(subnode, ast.Name):
            if subnode.id not in _safe_names and subnode.id not in variables:
                raise ValueError("Unsafe expression {}. contains {}".format(expr, subnode.id))
        if subnode_name not in _safe_nodes:
            raise ValueError("Unsafe expression {}. contains {}".format(expr, subnode_name))

    return eval(expr, variables)



class SafeEvalTests(unittest.TestCase):

    def test_basic(self):
        self.assertEqual(safe_eval("1", {}), 1)

    def test_local(self):
        self.assertEqual(safe_eval("a", {'a': 2}), 2)

    def test_local_bool(self):
        self.assertEqual(safe_eval("a==2", {'a': 2}), True)

    def test_lambda(self):
        self.assertRaises(ValueError, safe_eval, "lambda : None", {'a': 2})

    def test_bad_name(self):
        self.assertRaises(ValueError, safe_eval, "a == None2", {'a': 2})

    def test_attr(self):
        self.assertRaises(ValueError, safe_eval, "a.__dict__", {'a': 2})

    def test_eval(self):
        self.assertRaises(ValueError, safe_eval, "eval('os.exit()')", {})

    def test_exec(self):
        self.assertRaises(SyntaxError, safe_eval, "exec 'import os'", {})

    def test_multiply(self):
        self.assertRaises(ValueError, safe_eval, "'s' * 3", {})

    def test_power(self):
        self.assertRaises(ValueError, safe_eval, "3 ** 3", {})

    def test_comprehensions(self):
        self.assertRaises(ValueError, safe_eval, "[i for i in [1,2]]", {'i': 1})

2
为什么排除了乘法? - KlausF
此外,div被排除在外,有任何特定原因吗? - Abishek Kumar
属性访问仍然可能,这使得它不安全,并且并没有真正回答楼主的问题。 - undefined

8

我认为Python并没有针对不受信任的代码设计任何安全措施。以下是一种在官方Python 2解释器中通过堆栈溢出(在C堆栈上)引发段错误的简单方法:

eval('()' * 98765)

以下是我对“返回SIGSEGV的最短代码”Code Golf问题的答案


在Python 3中,这会导致最大递归深度超过限制。如果你的Python 2没有抛出异常或崩溃,那么你需要增加这个数字!我在一个系统上需要987650。 - Antti Haapala -- Слава Україні
你的原始代码在那里崩溃了,哈哈:D - Antti Haapala -- Слава Україні
我非常喜欢Python 3使用RecursionError来处理问题,而不是出现段错误。你可以捕获RecursionError,但要捕获段错误则更加困难。 - hlongmore

1
控制localsglobals字典非常重要。否则,有人可以传递evalexec,并递归调用它。
safe_eval('''e("""[c for c in ().__class__.__base__.__subclasses__() 
    if c.__name__ == \'catch_warnings\'][0]()._module.__builtins__""")''', 
    globals={'e': eval})

在递归eval中,表达式只是一个字符串。
您还需要将全局命名空间中的eval和exec名称设置为不是真正的eval或exec。全局命名空间很重要。如果使用本地命名空间,则任何创建单独命名空间的内容,例如理解和lambda,都将绕过它。
safe_eval('''[eval("""[c for c in ().__class__.__base__.__subclasses__()
    if c.__name__ == \'catch_warnings\'][0]()._module.__builtins__""") for i in [1]][0]''', locals={'eval': None})

safe_eval('''(lambda: eval("""[c for c in ().__class__.__base__.__subclasses__()
    if c.__name__ == \'catch_warnings\'][0]()._module.__builtins__"""))()''',
    locals={'eval': None})

在这里,safe_eval 只看到了一个字符串和一个函数调用,而没有属性访问。

如果 safe_eval 函数本身有禁用安全解析的标志,您还需要清除它。否则,您可以简单地执行以下操作:

safe_eval('safe_eval("<dangerous code>", safe=False)')

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接