Python eval:如果我禁用内置和属性访问,它仍然很危险吗?



我们都知道eval是危险的,即使你隐藏了危险的函数,因为你可以使用Python的内省功能来挖掘事物并重新提取它们。例如,即使您删除了__builtins__,也可以使用

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

但是,我看到的每个示例都使用属性访问。如果我禁用所有内置并禁用属性访问(通过使用 Python 标记器标记输入并在具有属性访问令牌时拒绝它)怎么办?

在你问之前,不,对于我的用例,我不需要这些中的任何一个,所以它不会太严重。

我想做的是使SymPy的sympify函数更加安全。目前,它标记输入,对其进行一些转换,并在命名空间中对其进行评估。但它是不安全的,因为它允许属性访问(即使它真的不需要它)。

我要提到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      '' 
可以从

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...的嵌套元组;该值不能print ed(在Python 3上),str ed或repr ed;所有调试它的尝试都会导致

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)')

使用 SIGSEGV 使 Python 2 崩溃。这是错误跟踪器中的 WONTFIX。因此,如果你想安全,永远不要使用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 仅接受安全修补程序。

因此可以得出结论,在 Python 中执行来自第三方的任何代码,无论是否经过净化,都永远不能被认为是安全的。

Nick Coghlan接着补充说:

作为为什么由Python代码引起的分段错误目前不被视为安全漏洞的一些额外背景:由于CPython不包含安全沙箱,我们已经完全依赖操作系统来提供进程隔离。 该 OS 级别安全边界不受代码是"正常"运行,还是在故意触发分段错误后处于修改状态的影响。

例如,用户仍然可以通过输入一个计算结果为巨大数字的表达式来DoS你,这会填满你的内存并使Python进程崩溃

'10**10**100'

我肯定仍然很好奇这里是否有可能进行更传统的攻击,例如恢复内置或创建段错误。

编辑:

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

lambda: 10**10**100

将挂起,因为它尝试预先计算常量。

下面是一个safe_eval示例,它将确保计算的表达式不包含不安全的标记。它不会尝试采用解释 AST 的literal_eval方法,而是将令牌类型列入白名单,并在表达式通过测试时使用真正的 eval。

# 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})
我不

相信Python旨在针对不受信任的代码提供任何安全性。以下是在官方 Python 2 解释器中通过堆栈溢出(在 C 堆栈上)诱导段错误的简单方法:

eval('()' * 98765)

从我对"返回 SIGSEGV 的最短代码"代码高尔夫问题的回答。

控制localsglobals字典非常重要。否则,有人可以传入evalexec,并递归地调用它

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

递归eval中的表达式只是一个字符串。

您还需要将全局命名空间中的evalexec名称设置为不是真正的evalexec。全局命名空间很重要。如果您使用本地命名空间,则创建单独命名空间的任何内容(例如推导式和 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)')

最新更新