【问题标题】:Python eval: is it still dangerous if I disable builtins and attribute access?Python eval:如果我禁用内置函数和属性访问,它仍然很危险吗?
【发布时间】:2016-06-18 17:08:29
【问题描述】:

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

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

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

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

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

【问题讨论】:

  • 这取决于你所说的危险......我想攻击者可以创建一个表达式来制作一个 really 大整数,这会导致你内存不足....
  • @mgilson 这是一个有效的观点。我想可以通过在您的应用程序上设置内存/时间保护来防止这种情况发生,但绝对值得注意。
  • 我认为这也取决于您传入的本地人...a + b 仅与a.__add__b.__radd__ 一样安全...
  • ast.literal_eval 的可能性还是您需要更多但仍然不需要属性?电话呢?

标签: python eval python-internals


【解决方案1】:

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

'10**10**100'

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

编辑:

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

lambda: 10**10**100

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

【讨论】:

  • 避免这种情况的唯一方法是使用超时来阻止正在运行的线程在 x 次之后或执行过多分配时的执行(这可能很难做到...... .)
  • @Bakuriu:如果你在 Python 中工作,那将会更加困难,因为这可能会在持有 GIL 时进行评估。对于这么大的数字,OOMing 的可能性也非零,具体取决于具体情况。
【解决方案2】:

如果您尝试printlog,则可以从eval 构造一个返回值,该值将引发异常 外部 evalrepr,随便:

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... 形式的嵌套元组;该值不能是 printed(在 Python 3 上)、stred 或 repred;所有调试它的尝试都会导致

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中的print2实际上并没有使用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 崩溃This is WONTFIX in the bug tracker。因此,如果您想安全起见,切勿使用print-the-statement。 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 的子类。解析器有一些really conservative limits to avoid crashes from stackoverflows(双关语)。但是s_push: parser stack overflow是C代码输出到stderr的,不能被抑制。


就在昨天我问why doesn't Python 3.4 be fixed for a crash from

% 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's answer 确认 Python 核心开发人员不会将看似格式良好的代码上的 SIGSEGV 视为安全问题:

3.4 仅接受安全修复。

因此可以得出结论,在 Python 中执行任何来自 3rd 方的代码,无论是否经过清理,都不能被认为是安全的。

然后Nick Coghlan 然后added

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

【讨论】:

  • "因此,没有安全的方法可以将此值转储到日志或任何内容中 - 任何尝试都会导致进一步的异常被抛出。" 有错误吗?
  • 看,Haskell 没有这个问题 :-D 即使是最奇怪的东西,要么触底,很容易被抓住,要么串成一个普通的无限长字符串,你可以打印一个任意长的部分的。
  • 第一个可以用f-strings在3.6中实现,不需要eval。
  • @AnttiHaapala 接受的答案表明这正是 f-strings 的目的。
  • 顺便说一句,nonlocal __x 问题已在 3.6.0a0 中得到修复,尽管奇怪的是它仍然存在于 3.5 的打包 (apt) 版本中,尽管修复已经 2 个月了跨度>
【解决方案3】:

我将提到 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      '' 

【讨论】:

  • 好吧,您只需要考虑所有 f 字符串的内容并检查它们(或者更安全:禁止它们)。
  • 这确实突出了试图保护eval 的移动目标的重要性。现在,它是 f 弦。谁知道3.7会带来什么?
  • 虽然它没有出现在 stdlib 的 tokenize 模块中,但在解析 f"{some code}" 时,f 字符串中的表达式将出现在 AST 中。
  • f-string 中的表达式不必包含任何属性访问节点,以便 f-string 进行属性访问 - 例如f"{eval('()' + chr(46) + '__class__')}".
【解决方案4】:

我不相信 Python 的设计目的是针对不受信任的代码提供任何安全性。这是在官方 Python 2 解释器中通过堆栈溢出(在 C 堆栈上)引发段错误的简单方法:

eval('()' * 98765)

从我的answer 到“返回 SIGSEGV 的最短代码”代码高尔夫问题。

【讨论】:

  • 在 Python 3 中,这会超出最大递归深度。如果您的 Python 2 没有给出异常或崩溃,则需要增加数量!我在 1 个系统上需要 987650。
  • 你的原件确实在那里崩溃了:D
  • 我非常喜欢 Python 3 用RecursionError 处理它而不是段错误。你可以抓到RecursionError;试图捕捉一个段错误要困难得多。
【解决方案5】:

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

【讨论】:

    【解决方案6】:

    这是一个 safe_eval 示例,它将确保评估的表达式不包含不安全的标记。 它不会尝试采用 literal_eval 方法来解释 AST,而是将令牌类型列入白名单并在表达式通过测试时使用真正的 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})
    

    【讨论】:

    • 有什么理由排除乘法?
    • 另外,div 因任何特定原因被排除在外
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-03-22
    • 2020-08-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-03-19
    相关资源
    最近更新 更多