使用 exec 函数时需要注意的一些安全问题,exec安全问题,未经作者许可,禁止转载!


本文作者: 编橙之家 - mozillazg 。未经作者许可,禁止转载!
欢迎加入编橙之家 专栏作者。

众所周知,在 python 中可以使用 exec 函数来执行包含 python 源代码的字符串:

Python
>>> code = '''
   ...: a = "hello"
   ...: print(a)
   ...: '''
>>> exec(code)
hello
>>> a
'hello'

exec 函数的这个功能很是强大,慎用。如果一定要用的话,那么就需要注意一下下面这些安全相关的问题。

全局变量和内置函数

exec 执行的代码中,默认可以访问执行 exec 时的局部变量和全局变量, 同样也会修改全局变量。如果 exec 执行的代码是根据用户提交的数据生产的话,这种默认行为就是一个安全隐患。

如何更改这种默认行为呢?可以通过执行 exec 函数的时候再传两个参数的方式来 修改这种行为(详见 之前 关于 exec 的文章):

Python
>>> g = {}
>>> l = {'b': 'world'}
>>> exec('hello = "hello" + b', g, l)
>>> l
{'b': 'world', 'hello': 'helloworld'}
>>> g
{'__builtins__': {...}}
>>> hello
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
...
NameError: name 'hello' is not defined

如果要限制使用内置函数的话,可以在 globals 参数中定义一下 __builtins__ 这个 key:

Python
>>> g = {}
>>> l = {}
>>> exec('a = int("1")', g, l)
>>> l
{'a': 1}

>>> g = {'__builtins__': {}}
>>> exec('a = int("1")', g, l)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<string>", line 1, in <module>
NameError: name 'int' is not defined
>>>

现在我们限制了访问和修改全局变量以及使用内置函数,难道这样就万事大吉了吗? 然而并非如此,还是可以通过其他的方式来获取内置函数甚至 os.system 函数。

另辟蹊径获取内置函数和 os.system

通过函数对象:

Python
>>> def a(): pass
...
>>> a.__globals__['__builtins__']

>>> a.__globals__['__builtins__'].open
<built-in function open>

通过内置类型对象:

Python
>>> for cls in {}.__class__.__base__.__subclasses__():
...     if cls.__name__ == 'WarningMessage':
...         b = cls.__init__.__globals__['__builtins__']
...         b['open']
...
<built-in function open>
>>>

获取 os.system:

Python
>>> cls = [x for x in [].__class__.__base__.__subclasses__() if x.__name__ == '_wrap_close'][0]
>>> cls.__init__.__globals__['path'].os
<module 'os' from '/usr/local/var/pyenv/versions/3.5.1/lib/python3.5/os.py'>
>>>

对于这两种办法又如何应对呢? 一种办法就是禁止访问以 _ 开头的属性:

  • 如果可以控制 code 的生成,那么就在生成 code 的时候判断
  • 如果不能的话,可以通过 dis 模块分析生成的 code:
    Python
    >>> code = "[x for x in [].__class__.__base__.__subclasses__() if x.__name__ == '_wrap_close'][0].__init__.__globals__['path'].os.system('date')"
    >>> exec(code)
    Wed May 18 22:55:05 CST 2016
    >>>
    >>> import dis
    >>> dis.dis(code)
      1           0 LOAD_CONST               0 (<code object <listcomp> at 0x10722c270, file "<dis>", line 1>)
                  3 LOAD_CONST               1 ('<listcomp>')
                  6 MAKE_FUNCTION            0
                  9 BUILD_LIST               0
                 12 LOAD_ATTR                0 (__class__)
                 15 LOAD_ATTR                1 (__base__)
                 18 LOAD_ATTR                2 (__subclasses__)
                 21 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
                 24 GET_ITER
                 25 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
                 28 LOAD_CONST               2 (0)
                 31 BINARY_SUBSCR
                 32 LOAD_ATTR                3 (__init__)
                 35 LOAD_ATTR                4 (__globals__)
                 38 LOAD_CONST               3 ('path')
                 41 BINARY_SUBSCR
                 42 LOAD_ATTR                5 (os)
                 45 LOAD_ATTR                6 (system)
                 48 LOAD_CONST               4 ('date')
                 51 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
                 54 RETURN_VALUE
    >>>

从上面可以看出来,获取属性的操作是 LOAD_ATTR 操作。我们只需要检查 LOAD_ATTR 的名字有没有以下划线开头就可以了:

Python
>>> class Writer:
...     def __init__(self):
...         self.text = ''
...     def write(self, msg):
...         self.text += msg
...
>>> w = Writer()
>>> dis.dis(code, file=w)
>>> print(w.text)
  1           0 LOAD_CONST               0 (<code object <listcomp> at 0x1072ce300, file "<dis>", line 1>)
              3 LOAD_CONST               1 ('<listcomp>')
              6 MAKE_FUNCTION            0
              9 BUILD_LIST               0
             12 LOAD_ATTR                0 (__class__)
             15 LOAD_ATTR                1 (__base__)
             18 LOAD_ATTR                2 (__subclasses__)
             21 CALL_FUNCTION            0 (0 positional, 0 keyword pair)
             24 GET_ITER
             25 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             28 LOAD_CONST               2 (0)
             31 BINARY_SUBSCR
             32 LOAD_ATTR                3 (__init__)
             35 LOAD_ATTR                4 (__globals__)
             38 LOAD_CONST               3 ('path')
             41 BINARY_SUBSCR
             42 LOAD_ATTR                5 (os)
             45 LOAD_ATTR                6 (system)
             48 LOAD_CONST               4 ('date')
             51 CALL_FUNCTION            1 (1 positional, 0 keyword pair)
             54 RETURN_VALUE

>>> re.search(r'd+s+LOAD_ATTRs+d+s+(_[^)]+)', w.text)
<_sre.SRE_Match object; span=(264, 305), match='12 LOAD_ATTR                0 (__class__)'>

我所知道的使用 exec 函数时需要注意的安全问题就是这些了。 如果你还知道其他需要注意的安全问题的话,欢迎留言告知。

打赏支持我写出更多好文章,谢谢!

打赏作者

打赏支持我写出更多好文章,谢谢!

评论关闭