PythonEval:如果禁用内置和属性访问,是否仍然很危险?

PythonEval:如果禁用内置和属性访问,是否仍然很危险?,python,eval,python-internals,Python,Eval,Python Internals,我们都知道,即使您隐藏了危险的函数,因为您可以使用Python的内省特性深入挖掘并重新提取它们。例如,即使删除了\uuuuu内置项,也可以使用 [c for c in ().__class__.__base__.__subclasses__() if c.__name__ == 'catch_warnings'][0]()._module.__builtins__ 但是,我看到的每个示例都使用属性访问。如果禁用所有内置项并禁用属性访问(通过使用Python标记器标记输入,并在输入具有属性

我们都知道,即使您隐藏了危险的函数,因为您可以使用Python的内省特性深入挖掘并重新提取它们。例如,即使删除了
\uuuuu内置项
,也可以使用

[c for c in ().__class__.__base__.__subclasses__()  
 if c.__name__ == 'catch_warnings'][0]()._module.__builtins__
但是,我看到的每个示例都使用属性访问。如果禁用所有内置项并禁用属性访问(通过使用Python标记器标记输入,并在输入具有属性访问标记时拒绝输入),会怎么样

在你问之前,不,对于我的用例,我不需要这两个,所以它不会太严重


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

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

'10**10**100'
我仍然很好奇,在这里是否有可能进行更传统的攻击,比如恢复内置软件或创建segfault

编辑:

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

lambda: 10**10**100

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

可以从
eval
构造一个返回值,如果您试图
打印
log
repr
,则该返回值将在外部引发异常
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…
);该值不能
print
ed(在Python 3上)、
str
ed或
repr
ed;所有调试该值的尝试都将导致

RuntimeError: maximum recursion depth exceeded while getting the repr of a tuple
pprint
saferepr
也会失败:

...
  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中的
print
2实际上没有使用
str
/
repr
,因此由于缺乏递归检查,您没有任何安全性。也就是说,取上面lambda monster的返回值,您不能
str
repr
它,而是普通的
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。因此,如果您希望安全,请不要使用
print
-语句。
来自未来导入打印功能


这不是撞车,而是

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

并确认Python核心DEVS不考虑SIGSEV在看似良好的代码中的安全问题:

3.4只接受安全修复

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

然后:

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


我要提到Python 3.6的一个新特性

他们可以计算表达式

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

我不认为Python是为防止不可信代码而设计的。下面是一种通过官方Python 2解释器中的堆栈溢出(在C堆栈上)诱发segfault的简单方法:

eval(“()”*98765)

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

控制
局部
全局
字典是非常重要的。否则,有人可以直接传入
eval
exec
,并递归调用它

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\u eval
只看到一个字符串和一个函数调用,而不是属性访问

如果
safe\u eval
函数有一个禁用安全解析的标志,那么您还需要清除该函数本身

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

下面是一个安全评估示例,它将确保评估的表达式不包含不安全的标记。 它不尝试采用解释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})

这取决于你所说的“危险”是什么意思……我认为攻击者可以创建一个表达式来生成一个非常大的整数,这会导致内存不足……@mgilson这是一个有效的观点。我认为可以通过在应用程序上设置内存/时间保护来防止这种情况,但绝对值得注意。我认为他也依赖于你通过的本地人…<代码> A+B/<代码>只不过是安全的.A.SyAdAdthux和<代码> B.SyRADyth> 是安全的……是代码> AST。检查它们(或者更安全地说:不允许它们)。这确实突出了移动目标试图保护
eval
的程度。现在,它是f-strings。谁
safe_eval('safe_eval("<dangerous code>", safe=False)')
# 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})