资讯专栏INFORMATION COLUMN

Python: 受限制的 "函数调用"

Mr_houzi / 3394人阅读

摘要:需求背景最近在工作上遇到了一个比较特殊的需求为了安全设计一个函数或者装饰器然后用户在定义调用函数时只能访问到我们允许的内置变量和全局变量通过例子来这解释下上面的需求输出函数功能简单明了对于结果大家应该也不会有太大的异议分别是取得全局命名空间

需求背景

最近在工作上, 遇到了一个比较特殊的需求:

   为了安全, 设计一个函数或者装饰器, 然后用户在 "定义/调用" 函数时, 只能访问到我们允许的内置变量和全局变量

通过例子来这解释下上面的需求:

a = 123
def func():
    print  a
    print id(a)

func()   

# 输出
123
32081168

函数功能简单明了, 对于结果, 大家应该也不会有太大的异议:func分别是取得全局命名空间a的值和使用内置命名空间中的函数id获取了a的地址. 熟悉Python的童鞋, 对于LEGB肯定也是不陌生的,也正是因为LEGB才让函数func输出正确的结果. 但是这个只是一个常规例子, 只是用来抛砖引玉而已. 我们真正想要讨论的是下面的例子:

# 装饰函数
def wrap(f):
    # 调用用户传入的函数
    f()

a = 123

# 用户自定义函数
def func():
    import os
    print os.listdir(".")

wrap(func)
# 输出
["1.yml", "2.py", "2.txt", "2.yml", "ftp", "ftp.rar", "test", "tmp", "__init__.py"]
潜在危险因素

在上面的例子可以看出, 如果在func中, 引入别的模块, 然后再执行模块中的方法, 也是可行的! 而且这还是一个非常方便的功能! 但是除了方便, 更多的是一种潜在的危险.在日常使用, 或许我们不会考虑这些, 但是如果在模块模块之间的协同作用时, 特别是多人参与的情况下, 这种危险的因素, 就不得不让我们认真对待!

或许有很多同学会觉得这些担忧是过多的, 是没必要的, 但是请思考一种场景: 我们有个主模块, 暂时称为main.py, 它允许用户动态加载模块, 也就是说只要用户将对应的模块放到对应的目录, 然后利用消息机制去通知main.py, 告诉它应该加载新模块了, 并且执行新模块里面的b函数, 那在这种情况下, main.py肯定不能直接傻傻的就去执行, 因为我们不能相信每个用户都是诚实善良的, 也不能相信每个用户编写的模块或者函数是符合我们的行为标准规范. 所以我们得有些措施去防范这些事情, 我们能做的大概也就下面几种方式:

1.在用户通知`main.py`时有新模块加入并且要求执行函数时, 先对模块的代码做检查, 不符合标准或者带有危险代码的拒绝加载.
2.控制好`内置命名空间`和`全局命名空间`, 使其只能用允许使用的内容

在方案1, 其实也是我们最容易想到的方法, 但是这个方法的成本还是比较高, 因为我们需要将可能出现的错误代码或者关键词,全部写成一套规则, 而且这套规则还很大可能会误伤, 不过也可能业界已经有类似的成熟的方案, 只是我还没接触到而已.
所以我们只能用方案2的方法, 这种方法在我们看来, 是成本比较低的, 也比较容易控制的, 因为这就和防火墙一样, 我们只放行我们允许的事物.

具体实现

实现方案2最大的问题就是, 如何控制内置命名空间全局命名空间
我们第一个想法肯定就是覆盖它们, 因为我们都知道不管是内置命名空间还是全局命名空间, 都是通过字典的形式在维护:

print globals()
print globals()["__builtins__"].__dict__

# 输出
# 全局命名空间
{"__builtins__": , "__name__": "__main__", "__file__": "D:/Python_project/ftp/2.py", "__doc__": None, "__package__": None}

#内置命名空间
{"bytearray": , "IndexError": 

注: globals函数 是用来打印当前全局命名空间的函数, 同样, 也能通过修改这个函数返回的字典对应的key, 实现全局命名空间的修改.例如:

s = globals()
print s
s["a"] = 3
print s
print a

# 输出
{"__builtins__": , "__file__": "D:/Python_project/ftp/2.py", "__package__": None, "s": {...}, "__name__": "__main__", "__doc__": None}
{"a": 3, "__builtins__": , "__file__": "D:/Python_project/ftp/2.py", "__package__": None, "s": {...}, "__name__": "__main__", "__doc__": None}
3

可以看出, 我们并没有定义变量a, 只是在globals的返回值上面增加了key-value, 就变相实现了我们定义的操作, 这其实也能用于很多希望能够动态赋值的需求场景! 比如说, 我不确定有多少个变量, 希望通过一个变量名列表, 动态生成这些变量, 在这种情况下, 就能参考这种方法, 不过还是希望谨慎使用, 因为修改了这个, 就是就修改了全局命名空间.

好了, 回归到本文, 我们已经知道通过globals函数能够代表全局命名空间, 但是为什么内置命名空间要用globals()["__builtins__"].__dict__来表示? 其实这个和python自身的机制有关, 因为模块在编译和初始化的过程中, 内置命名空间就是以这种形式,寄放在全局命名空间:

static void
initmain(void)
{
    PyObject *m, *d;
    m = PyImport_AddModule("__main__");
    if (m == NULL)
        Py_FatalError("can"t create __main__ module");
    d = PyModule_GetDict(m);
    if (PyDict_GetItemString(d, "__builtins__") == NULL) {
        PyObject *bimod = PyImport_ImportModule("__builtin__");
        if (bimod == NULL ||
            PyDict_SetItemString(d, "__builtins__", bimod) != 0)
            Py_FatalError("can"t add __builtins__ to __main__");
        Py_XDECREF(bimod);
    }
}

从上面代码可以看出, 在初始化__main__时, 会有一个获取__builtins__的动作, 如果这个结果是NULL, 那么就会用之前初始化好的__builtin__去存进去, 这些代码具体可以看Pythonrun.c, 在这不详细展开了.

既然内置命名空间(__builtins__)全局命名空间(globals())都已经找到对应对象了, 那我们下一步就应该是想法将这两个空间替换成我们想要的.

# coding: utf8
# 修改全局命名空间
test_var = 123  # 测试变量

tmp = globals().keys()
print globals()
print test_var
for i in tmp:
    del globals()[i]
print globals()
print test_var
print id(2)

# 输出

{"tmp": ["__builtins__", "__file__", "__package__", "test_var", "__name__", "__doc__"], "__builtins__": , "__file__": "D:/Python_project/ftp/2.py", "__package__": None, "test_var": 123, "__name__": "__main__", "__doc__": None}
123
{"tmp": ["__builtins__", "__file__", "__package__", "test_var", "__name__", "__doc__"], "i": "__doc__"}
Traceback (most recent call last):
  File "D:/Python_project/ftp/2.py", line 10, in 
    print test_var
NameError: name "test_var" is not defined

在上面的输出可以看到, 在删除前后, 通过print globals()可以看到全局命名空间确实已经被修改了, 因为test_var已经无法打印了, 触发了NameError, 这样的话, 就有办法能够限制全局命令空间了:

# 伪代码

# 装饰函数
def wrap(f):
    # 调用用户传入的函数
    .... 修改全局命名空间
    f()
    .... 还原全局命名空间

a = 123

# 用户自定义函数
def func():
    import os
    print os.listdir(".")

wrap(func)

为什么我只写伪代码, 因为我发现这个功能实现起来是非常蛋疼! 原因就是, 在实现之前, 我们必须要解决几个问题:

1.全局命名空间对应了一个字典, 所以如果我们想要修改, 只能从修改这个字典本身, 于是先清空再定义成我们约束的, 调用完之后, 又得反过来恢复, 这些操作是十分之蛋疼.
2.涉及到共享的问题, 如果这个用户函数处理很久, 而且是多线程的, 那么整个模块都会变得很不稳定, 甚至称为"污染"

那就先撇开不讲, 讲讲内置命名空间, 刚才我们已经找到了能代表内置命名空间的对象, 很幸运的是, 这个是"真的能够摸得到"的, 那我们试下直接就赋值个空字典, 看会怎样:

s = globals()
print s["__builtins__"]  # __builtins__检查是否存在
s["__builtins__"] = {}
print s["__builtins__"]  # __builtins__检查是否存在
print id(3)              # 试下内置函数能否使用
print globals()

# 输出

{}
32602360
{"__builtins__": {}, "__file__": "D:/Python_project/ftp/2.py", "__package__": None, "s": {...}, "__name__": "__main__", "__doc__": None}

结果有点尴尬, 似乎没啥用, 但是其实这个__builtins__只是一个表现, 真正的内置命名空间是在它所指向的字典对象, 也就是: globals()["__builtins__"].__dict__!

print globals()["__builtins__"].__dict__

# 输出
{"bytearray": , "IndexError": ....} # 省略

所以我们真正要覆盖的, 是这个字典才对, 所以上面的代码要改成:

s = globals()
s["__builtins__"].__dict__ = {}   # 覆盖真正的内置命名空间
print s["__builtins__"].__dict__  # __builtins__检查是否存在

# 输出
Traceback (most recent call last):
  File "D:/Python_project/ftp/2.py", line 3, in 
    s["__builtins__"].__dict__ = {}
TypeError: readonly attribute

失败了...原来这个内置命名空间是只读的, 所以我们上面的方法都失败了..那难道真的没法解决了吗? 一般这样问, 通常都有解决方案滴~

完美方案

这个解决方法, 需要一个库的帮忙~, 那就是inspect库, 这个库是干嘛呢? 简单来说就是用来自省. 它提供四种用处:

1.对是否是模块,框架,函数等进行类型检查。
2.获取源码
3.获取类或函数的参数的信息
4.解析堆栈

在这里, 我们需要用到第二个功能, 其余的功能, 感兴趣的童鞋可以去谷歌学习哦, 也可以参考: https://my.oschina.net/taisha...
除了inspect, 我们还需要用到exec, 这也是一大杀器, 可以先参考这个学习下: http://www.mojidong.com/pytho...

方法大致的过程就是以下几步:

1.根据用户传入的func对象, 利用inspect取出对应的源码
2.通过exec利用源码并且传入全局命名空间, 重新编译

代码:

# coding: utf8
import inspect

# 装饰函数
def wrap(f):
    # 调用用户传入的函数
    source = inspect.getsource(f)   # 获取源码
    exec("%s 
%s()" % (source,  f.func_name), {"a": "this is inspect", "__builtins__": {}})  # 重新编译, 并且重新构造全局命名空间


a = 123

# 用户自定义函数
def func():
    print a
    import os
    print os.listdir(".")

wrap(func)

# 输出
this is inspect
Traceback (most recent call last):
  File "D:/Python_project/ftp/2.py", line 19, in 
    wrap(func)
  File "D:/Python_project/ftp/2.py", line 8, in wrap
    exec("%s 
func()" % source, {"a": "this is inspect", "__builtins__": {}})
  File "", line 6, in 
  File "", line 3, in func
ImportError: __import__ not found

虽然上面报错了, 但那不就我们求之不得结果吗? 我们可以正确的输出a的值this is inspe, 而且当funcimport时, 直接报错! 这样就能满足我们的{{BANNED}}欲望了~ 嘿嘿!,

关于代码运行原理, 其实在关键部位的代码, 都已经加了注释, 可能在exec那部分会比较迷惑, 但其实大家将对应的变量代入字符串就能懂了, 替换之后, 其实也就是函数的定义+执行, 可以通过print "%s %s()" % (source, f.func_name)帮助理解.而后面的字典, 也就是我们一直很纠结的全局命名空间, 其中内置命名空间也被人为定义了, 所以能够达到我们想要的效果了!

这种只是一种抛砖引玉, 让有类似场景需求的童鞋, 有个参考的方向, 也欢迎分享你们实现的方案, 嘿嘿!

欢迎各位大神指点交流,转载请注明来源: https://segmentfault.com/a/11...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/38621.html

相关文章

  • 处理python递归函数及递归算法频次限制难题

      本文关键阐述了处理python递归函数及递归算法频次受限制难题,具有非常好的实用价值,希望能帮助到大家。如有误或者未考虑到真正的地区,望鼎力相助  递归函数及递归算法频次受限制  一个函数在外部启用自身,那么这样的函数是递归函数。递归算法要反复应用自身,每递归算法一回,越近最后的值。如果一个难题需要由很多类似小问题处理,可以选择应用递归函数。伴随着递归算法的深层次,难题经营规模对比之前都应该所...

    89542767 评论0 收藏0
  • pythonGUI多做输入文本Text完成

      文章主要是详细介绍了pythonGUI多做输入文本Text的控制方式,具有非常好的实用价值,希望能帮助到大家。如有误或者未考虑到真正的地区,望鼎力相助  Text的属性wrap  fromtkinterimport*   root=Tk()   root.geometry('200x300')   te=Text(root,height=20,width=15)   #将多做输...

    89542767 评论0 收藏0
  • pythonGUI多列输入文本Text完成

      此篇文章主要是详细介绍了pythonGUI多列输入文本Text的控制方式,具有非常好的实用价值,希望能帮助到大家。如有误或者未考虑到真正的地区,望鼎力相助  Text的属性wrap  fromtkinterimport*   root=Tk()   root.geometry('200x300')   te=Text(root,height=20,width=15)   #将多...

    89542767 评论0 收藏0
  • 文章彻底搞懂Python类属性和方法开启

      对python调用类特性方式详细描述检验前提下类开启也经常需要用到的,下面文中重要给大家介绍了相关Python类属性和方法的开启的相关资料,从文中根据实例编号介绍的非常详细,务必的朋友可以参考一下  Python从技术的时候就已经已是一类面向对象语言表述,也正因为如此,在Python中打造一个类和对象是非常简单的。  一、类、总体目标概述  在C语言程序设计中,把数据和信息以及对业务操作流程封...

    89542767 评论0 收藏0
  • Python标准库sys库常用功能相关解答

      小编写这篇文章的主要目的,就是给大家介绍关于Python标准库sys常用功能的一些介绍,这样对我们以后的工作也是很有帮助的,具体的介绍,下面就给大家详细解答下。  1、查看版本信息  #coding:utf-8   importsys   #获取Python版本信息   print(sys.version)   #获取解释器中C的API版本   print(sys.api_version)  ...

    89542767 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<