深度辨析Python的eval()与exec()的方法


Posted in Python onMarch 26, 2019

Python 提供了很多内置的工具函数(Built-in Functions),在最新的 Python 3 官方文档中,它列出了 69 个。

大部分函数是我们经常使用的,例如 print()、open() 与 dir(),而有一些函数虽然不常用,但它们在某些场景下,却能发挥出不一般的作用。内置函数们能够被“提拔”出来,这就意味着它们皆有独到之处,有用武之地。

因此,掌握内置函数的用法,就成了我们应该点亮的技能。

在《Python进阶:如何将字符串常量转为变量? 》这篇文章中,我提到过 eval() 和 exec() ,但对它们并不太了解。为了弥补这方面知识,我就重新学习了下。这篇文章是一份超级详细的学习记录,系统、全面而深入地辨析了这两大函数。

1、eval 的基本用法

语法:eval(expression, globals=None, locals=None)

它有三个参数,其中 expression 是一个字符串类型的表达式或代码对象,用于做运算;globals 与 locals 是可选参数,默认值是 None。

具体而言,expression 只能是单个表达式,不支持复杂的代码逻辑,例如赋值操作、循环语句等等。(PS:单个表达式并不意味着“简单无害”,参见下文第 4 节)

globals 用于指定运行时的全局命名空间,类型是字典,缺省时使用的是当前模块的内置命名空间。locals 指定运行时的局部命名空间,类型是字典,缺省时使用 globals 的值。两者都缺省时,则遵循 eval 函数执行时的作用域。值得注意的是,这两者不代表真正的命名空间,只在运算时起作用,运算后则销毁。

x = 10

def func():
  y = 20
  a = eval('x + y')
  print('a: ', a)
  b = eval('x + y', {'x': 1, 'y': 2})
  print('x: ' + str(x) + ' y: ' + str(y))
  print('b: ', b)
  c = eval('x + y', {'x': 1, 'y': 2}, {'y': 3, 'z': 4})
  print('x: ' + str(x) + ' y: ' + str(y))
  print('c: ', c)

func()

输出结果:
a:  30
x: 10 y: 20
b:  3
x: 10 y: 20
c:  4

由此可见,当指定了命名空间的时候,变量会在对应命名空间中查找。而且,它们的值不会覆盖实际命名空间中的值。

2、exec 的基本用法

语法:exec(object[, globals[, locals]])

在 Python2 中 exec 是个语句,而 Python3 将其改造成一个函数,就像 print 一样。exec() 与 eval() 高度相似,三个参数的意义和作用相近。

主要的区别是,exec() 的第一个参数不是表达式,而是代码块,这意味着两点:一是它不能做表达式求值并返回出去,二是它可以执行复杂的代码逻辑,相对而言功能更加强大,例如,当代码块中赋值了新的变量时,该变量可能 在函数外的命名空间中存活下来。

>>> x = 1
>>> y = exec('x = 1 + 1')
>>> print(x)
>>> print(y)
2
None

可以看出,exec() 内外的命名空间是相通的,变量由此传递出去,而不像 eval() 函数,需要一个变量来接收函数的执行结果。

3、一些细节辨析

两个函数都很强大,它们将字符串内容当做有效的代码执行。这是一种字符串驱动的事件 ,意义重大。然而,在实际使用过程中,存在很多微小的细节,此处就列出我所知道的几点吧。

常见用途:将字符串转成相应的对象,例如 string 转成 list ,string 转成 dict,string 转 tuple 等等。

>>> a = "[[1,2], [3,4], [5,6], [7,8], [9,0]]"
>>> print(eval(a))
[[1, 2], [3, 4], [5, 6], [7, 8], [9, 0]]
>>> a = "{'name': 'Python猫', 'age': 18}"
>>> print(eval(a))
{'name': 'Python猫', 'age': 18}

# 与 eval 略有不同
>>> a = "my_dict = {'name': 'Python猫', 'age': 18}"
>>> exec(a)
>>> print(my_dict)
{'name': 'Python猫', 'age': 18}

eval() 函数的返回值是其 expression 的执行结果,在某些情况下,它会是 None,例如当该表达式是 print() 语句,或者是列表的 append() 操作时,这类操作的结果是 None,因此 eval() 的返回值也会是 None。

>>> result = eval('[].append(2)')
>>> print(result)
None

exec() 函数的返回值只会是 None,与执行语句的结果无关,所以,将 exec() 函数赋值出去,就没有任何必要。所执行的语句中,如果包含 return 或 yield ,它们产生的值也无法在 exec 函数的外部起作用。

>>> result = exec('1 + 1')
>>> print(result)
None

两个函数中的 globals 和 locals 参数,起到的是白名单的作用,通过限定命名空间的范围,防止作用域内的数据被滥用。

conpile() 函数编译后的 code 对象,可作为 eval 和 exec 的第一个参数。compile() 也是个神奇的函数,我翻译的上一篇文章《Python骚操作:动态定义函数 》就演示了一个动态定义函数的操作。

吊诡的局部命名空间:前面讲到了 exec() 函数内的变量是可以改变原有命名空间的,然而也有例外。

def foo():
  exec('y = 1 + 1\nprint(y)')
  print(locals())
  print(y)

foo()

按照前面的理解,预期的结果是局部变量中会存入变量 y,因此两次的打印结果都会是 2,然而实际上的结果却是:

2
{'y': 2}
Traceback (most recent call last):
...(略去部分报错信息)
    print(y)
NameError: name 'y' is not defined

明明看到了局部命名空间中有变量 y,为何会报错说它未定义呢?

原因与 Python 的编译器有关,对于以上代码,编译器会先将 foo 函数解析成一个 ast(抽象语法树),然后将所有变量节点存入栈中,此时 exec() 的参数只是一个字符串,整个就是常量,并没有作为代码执行,因此 y 还不存在。直到解析第二个 print() 时,此时第一次出现变量 y ,但因为没有完整的定义,所以 y 不会被存入局部命名空间。

在运行期,exec() 函数动态地创建了局部变量 y ,然而由于 Python 的实现机制是“运行期的局部命名空间不可改变 ”,也就是说这时的 y 始终无法成为局部命名空间的一员,当执行 print() 时也就报错了。

至于为什么 locals() 取出的结果有 y,为什么它不能代表真正的局部命名空间?为什么局部命名空间无法被动态修改?可以查看我之前分享的《Python 动态赋值的陷阱 》,另外,官方的 bug 网站中也有对此问题的讨论,查看地址:https://bugs.python.org/issue4831

若想把 exec() 执行后的 y 取出来的话,可以这样:z = locals()['y'] ,然而如果不小心写成了下面的代码,则会报错:

def foo():
  exec('y = 1 + 1')
  y = locals()['y']
  print(y)

foo()

#报错:KeyError: 'y'
#把变量 y 改为其它变量则不会报错

KeyError 指的是在字典中不存在对应的 key 。本例中 y 作了声明,却因为循环引用而无法完成赋值,即 key 值对应的 value 是个无效值,因此读取不到,就报错了。

此例还有 4 个变种,我想用一套自恰的说法来解释它们,但尝试了很久,未果。留个后话吧,等我想明白,再单独写一篇文章。

4、为什么要慎用 eval() ?

很多动态的编程语言中都会有 eval() 函数,作用大同小异,但是,无一例外,人们会告诉你说,避免使用它。

为什么要慎用 eval() 呢?主要出于安全考虑,对于不可信的数据源,eval 函数很可能会招来代码注入的问题。

>>> eval("__import__('os').system('whoami')")
desktop-fa4b888\pythoncat
>>> eval("__import__('subprocess').getoutput('ls ~')")
#结果略,内容是当前路径的文件信息

在以上例子中,我的隐私数据就被暴露了。而更可怕的是,如果将命令改为rm -rf ~ ,那当前目录的所有文件都会被删除干净。

针对以上例子,有一个限制的办法,即指定 globals 为 {'__builtins__': None} 或者 {'__builtins__': {}} 。

>>> s = {'__builtins__': None}
>>> eval("__import__('os').system('whoami')", s)
#报错:TypeError: 'NoneType' object is not subscriptable

__builtins__ 包含了内置命名空间中的名称,在控制台中输入 dir(__builtins__) ,就能发现很多内置函数、异常和其它属性的名称。在默认情况下,eval 函数的 globals 参数会隐式地携带__builtins__ ,即使是令 globals 参数为 {} 也如此,所以如果想要禁用它,就得显式地指定它的值。

上例将它映射成 None,就意味着限定了 eval 可用的内置命名空间为 None,从而限制了表达式调用内置模块或属性的能力。

但是,这个办法还不是万无一失的,因为仍有手段可以发起攻击。

某位漏洞挖掘高手在他的博客中分享了一个思路,令人大开眼界。其核心的代码是下面这句,你可以试试执行,看看输出的是什么内容。

>>> ().__class__.__bases__[0].__subclasses__()

关于这句代码的解释,以及更进一步的利用手段,详见博客。(地址:https://3water.com/article/158468.htm)

另外还有一篇博客,不仅提到了上例的手段,还提供了一种新的思路:

#警告:千万不要执行如下代码,后果自负。
>>> eval('(lambda fc=(lambda n: [c 1="c" 2="in" 3="().__class__.__bases__[0" language="for"][/c].__subclasses__() if c.__name__ == n][0]):fc("function")(fc("code")(0,0,0,0,"KABOOM",(),(),(),"","",0,""),{})())()', {"__builtins__":None})

这行代码会导致 Python 直接 crash 掉。具体分析在:https://3water.com/article/158470.htm

除了黑客的手段,简单的内容也能发起攻击。像下例这样的写法, 将在短时间内耗尽服务器的计算资源。

>>> eval("2 ** 888888888", {"__builtins__":None}, {})

如上所述,我们直观地展示了 eval() 函数的危害性,然而,即使是 Python 高手们小心谨慎地使用,也不能保证不出错。

在官方的 dumbdbm 模块中,曾经(2014年)发现一个安全漏洞,攻击者通过伪造数据库文件,可以在调用 eval() 时发起攻击。(详情:https://bugs.python.org/issue22885)

无独有偶,在上个月(2019.02),有核心开发者针对 Python 3.8 也提出了一个安全问题,提议不在 logging.config 中使用 eval() 函数,目前该问题还是 open 状态。(详情:https://bugs.python.org/issue36022)

如此种种,足以说明为什么要慎用 eval() 了。同理可证,exec() 函数也得谨慎使用。

5、安全的替代用法

既然有种种安全隐患,为什么要创造出这两个内置方法呢?为什么要使用它们呢?

理由很简单,因为 Python 是一门灵活的动态语言。与静态语言不同,动态语言支持动态地产生代码,对于已经部署好的工程,也可以只做很小的局部修改,就实现 bug 修复。

那有什么办法可以相对安全地使用它们呢?

ast 模块的 literal() 是 eval() 的安全替代,与 eval() 不做检查就执行的方式不同,ast.literal() 会先检查表达式内容是否有效合法。它所允许的字面内容如下:

strings, bytes, numbers, tuples, lists, dicts, sets, booleans, 和 None

一旦内容非法,则会报错:

import ast
ast.literal_eval("__import__('os').system('whoami')")

报错:ValueError: malformed node or string

不过,它也有缺点:AST 编译器的栈深(stack depth)有限,解析的字符串内容太多或太复杂时,可能导致程序崩溃。

至于 exec() ,似乎还没有类似的替代方法,毕竟它本身可支持的内容是更加复杂多样的。

最后是一个建议:搞清楚它们的区别与运行细节(例如前面的局部命名空间内容),谨慎使用,限制可用的命名空间,对数据源作充分校验。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Python 相关文章推荐
浅谈Python处理PDF的方法
Nov 10 Python
Python使用Matplotlib实现雨点图动画效果的方法
Dec 23 Python
python实现指定字符串补全空格、前面填充0的方法
Nov 16 Python
5分钟 Pipenv 上手指南
Dec 20 Python
python实现大转盘抽奖效果
Jan 22 Python
100行Python代码实现每天不同时间段定时给女友发消息
Sep 27 Python
python3反转字符串的3种方法(小结)
Nov 07 Python
python中Lambda表达式详解
Nov 20 Python
python图片指定区域替换img.paste函数的使用
Apr 09 Python
使用Pycharm分段执行代码
Apr 15 Python
Numpy数组的广播机制的实现
Nov 03 Python
解决pytorch 保存模型遇到的问题
Mar 03 Python
详解Python locals()的陷阱
Mar 26 #Python
python 为什么说eval要慎用
Mar 26 #Python
Python eval的常见错误封装及利用原理详解
Mar 26 #Python
Python骚操作之动态定义函数
Mar 26 #Python
python 将有序数组转换为二叉树的方法
Mar 26 #Python
浅谈Python爬虫基本套路
Mar 25 #Python
我用Python抓取了7000 多本电子书案例详解
Mar 25 #Python
You might like
PHP中date()日期函数有关参数整理
2011/07/19 PHP
laravel安装和配置教程
2014/10/29 PHP
php、java、android、ios通用的3des方法(推荐)
2016/09/09 PHP
tp5.0框架隐藏index.php入口文件及模块和控制器的方法分析
2020/02/11 PHP
javascript 浏览器判断 绑定事件 arguments 转换数组 数组遍历
2009/07/06 Javascript
jQuery 白痴级入门教程
2009/11/11 Javascript
JQuery与iframe交互实现代码
2009/12/24 Javascript
使用jQuery全局事件ajaxStart为特定请求实现提示效果的代码
2010/12/30 Javascript
js获取元素到文档区域document的(横向、纵向)坐标的两种方法
2013/05/17 Javascript
js创建一个input数组并绑定click事件的方法
2014/06/12 Javascript
浅谈jQuery中replace()方法
2015/05/13 Javascript
js实现的彩色方块飞舞奇幻效果
2016/01/27 Javascript
JS实现1000以内被3或5整除的数字之和
2016/02/18 Javascript
使用jQuery UI库开发Web界面的简单入门指引
2016/04/22 Javascript
jquery日历插件e-calendar升级版
2016/11/10 Javascript
详细讲解vue2+vuex+axios
2017/05/27 Javascript
浅谈vuex之mutation和action的基本使用
2017/08/29 Javascript
小程序实现图片预览裁剪插件
2019/11/22 Javascript
Vue v-bind动态绑定class实例方法
2020/01/15 Javascript
微信小程序实现音频文件播放进度的实例代码
2020/03/02 Javascript
[45:59]EG vs OG 2018国际邀请赛小组赛BO2 第二场 8.17
2018/08/18 DOTA
python Opencv将图片转为字符画
2021/02/19 Python
基于numpy.random.randn()与rand()的区别详解
2018/04/17 Python
python 读入多行数据的实例
2018/04/19 Python
python中tab键是什么意思
2020/06/18 Python
Tensorflow使用Anaconda、pycharm安装记录
2020/07/29 Python
python中实现栈的三种方法
2020/12/19 Python
Html5画布_动力节点Java学院整理
2017/07/13 HTML / CSS
html5调用摄像头功能的实现代码
2018/05/07 HTML / CSS
阿迪达斯西班牙官方网站:adidas西班牙
2016/07/21 全球购物
英国奢侈品网站:MatchesFashion
2016/12/16 全球购物
支票、地址标签、包装纸和慰问卡:Current Catalog
2018/01/30 全球购物
测绘工程系学生的自我评价
2013/11/30 职场文书
文秘自荐信
2014/06/28 职场文书
制定企业培训计划的五大要点!
2019/07/10 职场文书
如何用RabbitMQ和Swoole实现一个异步任务系统
2021/05/29 PHP