python3执行文本py代码的最佳实践

31 篇文章 2 订阅

关于文本代码执行,什么意思呢?简单打个比方,你在数据库里建了一个文本字段,在文本字段里存放的内容为一段python代码,系统可以直接运行这段代码,实现想要的功能,从此可以在前端编写一些脚本存放至数据库,不需要小需求都得改后端代码,岂不是无敌?

不得不说的是,这个方法实际效果出奇的好,python3内置exec函数,就是可以用来执行长串的python代码的。但是呢,必须要自己处理一下。

至于处理什么?主要有以下几点,都是出于安全性考虑。这几点处理完毕,自己就能做一个云函数功能出来了。反正我花费了两天时间,终于给肝出来了!

第一点,安全性。

何为安全性?要知道,python3的exec函数是完全支持本地环境的。如果不良用户利用这一点,在代码里加入恶意语句,比如import os,然后os.system执行一条语句 rm -rf ./

后果可想而知,这是干嘛?直接python3代码执行删除服务器所有文件,我勒个去。

所以,考虑到这一点,首先,禁止exec的代码里进行import导入包,我不允许你导入包,只能使用python3自带的保留字,看你能掀起什么浪?

因此,我们删除一些不安全的保留字,例如import,eval,exec之类的

因为open也是保留字,防止用户通过文本代码向服务器恶意写入垃圾文件,比如 with open('1.txt) as f,

f.write之类的,我们也给干掉

关键代码如下:

builtins = __builtins__
builtins = dict(builtins).copy()
for key in ['__import__', 'eval', 'exec', 'globals', 'dir', 'copyright', 'open', 'quit']:
    del builtins[key]  # 删除不安全的关键字

会了上面的代码,这下好了,危险的关键字无法使用了,python3文本代码里再有这种代码,执行的时候直接报错name undefiend了。

但是你以为这么简单就完事儿了吗?答案是否定的。

python3还有很多__下划线开头的隐私方法,可用同样把其他包导入进来为非作歹。

为此,我们需要干掉私有变量的调用,具体检测代码如下:

import tokenize
def check_unsafe_attributes(string):
    """
    安全检测需要exec执行的python代码
    :param string:
    :return:
    """
    g = tokenize.tokenize(io.BytesIO(string.encode('utf-8')).readline)
    pre_op = ''
    for toktype, tokval, _, _, _ in g:
        if toktype == tokenize.NAME and pre_op == '.' and tokval.startswith('_'):
            attr = tokval
            msg = "access to attribute '{0}' is unsafe.".format(attr)
            raise AttributeError(msg)
        elif toktype == tokenize.OP:
            pre_op = tokval

怎么用呢,在exec(目标代码)代码之前调用一下 check_unsafe_attributes(目标代码)

如果检测到目标代码有私有调用直接就抛错了然后不执行就完事儿。

这两步搞定过后,安全执行代码问题就没了,但是还有第二点问题。

第二点, 恶意死循环。

这一个问题着实恶心,比如有人在代码里写了while True,写了for的几十万次垃圾代码,这可如何是好?干掉while和for关键字?这样确实可以杜绝死循环,但是想实现完整的python功能可就太难了。如果不解决这个问题,用户调用云函数执行死循环,服务器后台那个线程就会一直运行着消耗资源,那如果死循环里有个log打印日志一直在跑,服务器上的日志文件都有可能把磁盘空间撑爆。所以,干掉log和print保留字?不,咱们不采取这个 办法。

综合上面情况,我采用了函数执行超时自动退出办法

什么意思呢?简单理解就是说,执行exec语句,我规定5秒内必须执行完毕,如果exec语句里面有死循环或者恶意循环代码,那5秒内一定执行不完毕,那么系统自动报超时错误,并且结束该线程。听这玩意儿是不是很高级?没错,但是需要引入三方模块,并且仅此一种办法可用,不要看网上的其他办法,我通通试过都不行。

pip install func_timeout
import func_timeout
from func_timeout import func_set_timeout

time_out_sec = 5
class my_exception(Exception):
    def __init__(self, message):
        self.message = message

    def __str__(self):
        message = f'函数执行超时: "{self.message}"'
        return message

@func_set_timeout(time_out_sec)
def excute(*args):
    exec(*args)

 这里重定义了一个 excute函数,目的就是超时执行exec函数,exec函数执行超过规定的时间自动被系统干掉并抛错。

核心代码讲解完毕了,接下来是实际关联数据库字段运行文本代码的按理。

环境采用python3,框架为odoo。

    def action_task_exec(self,call=None,params=None):
        """
        接口调用执行函数
        :return:
        """
        self.ensure_one()
        if not params:
            params = []
        env = self.env
        builtins = __builtins__
        builtins = dict(builtins).copy()
        for key in ['__import__','eval','exec','globals','dir','copyright','open','quit']:
            del builtins[key]  # 删除不安全的关键字
        # print(builtins)
        global_dict = {'__builtins__': builtins,
                       'requests': requests, 'urljoin':urljoin,'quote':quote,'unquote': unquote,
                       'log': _logger.info,
                       'env': env, 'cr': env.cr, 'uid': env.uid, 'error': UserError,
                       're':re,'etree':etree,'time':time,'datetime':datetime}  # 禁用内置函数,不允许导入包
        try:
            check_unsafe_attributes(self.pycode)
            localdict = {'result': None}
            # 待解决windows下运行超时的问题
            to_run_code = self.pycode.strip()
            params_str = ''
            for i in range(len(params)):
                par = params[i]
                if isinstance(par,str):
                    params_str+=f"'{par}'"
                else:
                    params_str+=f'{par}'
                if i != len(params)-1:
                    params_str+=','
            func = f'result={call}({params_str})' if call else False
            if func:
                _logger.info(f'开始执行:{func}')
                to_run_code = to_run_code+'\n'+func
            # exec(to_run_code, global_dict, localdict)
            try:
                excute(to_run_code, global_dict, localdict)
            except func_timeout.exceptions.FunctionTimedOut:
                raise my_exception(f'函数[{self.name}](id:{self.id})运行时间超过{time_out_sec}秒,疑似死循环,已被系统切断')
        except Exception as e:
            ret = f'执行报错:{e}'
            _logger.info(ret)
            return ret
        else:
            # print(global_dict)
            # print(localdict)
            ret = localdict['result']
            return ret

其中global_dict  是exec执行文本代码默认带的环境,干掉了一些保留字并且内置一些常用的函数比如requests,re,xpath。这样被执行的文本代码里不需要import导入包也能使用该对象

localdict 是存放本地执行exec完毕后得到的结果,里面就有你文本代码里赋值的内容

下面是执行案例:

 

 

 如图所示,云函数任务脚本里的文本代码为死循环打印日志

点击运行后,服务器后台疯狂飙日志

 但是5秒后归于大海

 前端执行结果如图:

 再看一个正常的函数执行:

 

 

 

大功告成,完美!再也不质疑自己的技术水平了,我就是最棒的!网上没有相关的例子资料,那我自己出这个教程吧,欢迎后来者参考~ 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值