深度辨析Python的eval()与exec()的方法
Python提供了很多内置的工具函数(Built-inFunctions),在最新的Python3官方文档中,它列出了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 deffunc(): 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:10y:20
b: 3
x:10y: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()函数内的变量是可以改变原有命名空间的,然而也有例外。
deffoo(): exec('y=1+1\nprint(y)') print(locals()) print(y) foo()
按照前面的理解,预期的结果是局部变量中会存入变量y,因此两次的打印结果都会是2,然而实际上的结果却是:
2
{'y':2}
Traceback(mostrecentcalllast):
...(略去部分报错信息)
print(y)
NameError:name'y'isnotdefined
明明看到了局部命名空间中有变量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'],然而如果不小心写成了下面的代码,则会报错:
deffoo(): 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'objectisnotsubscriptable
__builtins__包含了内置命名空间中的名称,在控制台中输入dir(__builtins__),就能发现很多内置函数、异常和其它属性的名称。在默认情况下,eval函数的globals参数会隐式地携带__builtins__,即使是令globals参数为{}也如此,所以如果想要禁用它,就得显式地指定它的值。
上例将它映射成None,就意味着限定了eval可用的内置命名空间为None,从而限制了表达式调用内置模块或属性的能力。
但是,这个办法还不是万无一失的,因为仍有手段可以发起攻击。
某位漏洞挖掘高手在他的博客中分享了一个思路,令人大开眼界。其核心的代码是下面这句,你可以试试执行,看看输出的是什么内容。
>>>().__class__.__bases__[0].__subclasses__()
关于这句代码的解释,以及更进一步的利用手段,详见博客。(地址:https://www.nhooo.com/article/158468.htm)
另外还有一篇博客,不仅提到了上例的手段,还提供了一种新的思路:
#警告:千万不要执行如下代码,后果自负。 >>>eval('(lambdafc=(lambdan:[c1="c"2="in"3="().__class__.__bases__[0"language="for"][/c].__subclasses__()ifc.__name__==n][0]):fc("function")(fc("code")(0,0,0,0,"KABOOM",(),(),(),"","",0,""),{})())()',{"__builtins__":None})
这行代码会导致Python直接crash掉。具体分析在:https://www.nhooo.com/article/158470.htm
除了黑客的手段,简单的内容也能发起攻击。像下例这样的写法,将在短时间内耗尽服务器的计算资源。
>>>eval("2**888888888",{"__builtins__":None},{})
如上所述,我们直观地展示了eval()函数的危害性,然而,即使是Python高手们小心谨慎地使用,也不能保证不出错。
在官方的dumbdbm模块中,曾经(2014年)发现一个安全漏洞,攻击者通过伪造数据库文件,可以在调用eval()时发起攻击。(详情:https://bugs.python.org/issue22885)
无独有偶,在上个月(2019.02),有核心开发者针对Python3.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
一旦内容非法,则会报错:
importast ast.literal_eval("__import__('os').system('whoami')")
报错:ValueError:malformednodeorstring
不过,它也有缺点:AST编译器的栈深(stackdepth)有限,解析的字符串内容太多或太复杂时,可能导致程序崩溃。
至于exec(),似乎还没有类似的替代方法,毕竟它本身可支持的内容是更加复杂多样的。
最后是一个建议:搞清楚它们的区别与运行细节(例如前面的局部命名空间内容),谨慎使用,限制可用的命名空间,对数据源作充分校验。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。