当前位置: 首页 > 后端技术 > Python

深入剖析Python的eval()和exec()_0

时间:2023-03-26 17:39:08 Python

Python提供了很多内置的工具函数(Built-inFunctions),在最新的Python3官方文档中,列出了其中的69个。大部分函数都是我们常用的,比如print()、open()和dir(),还有一些函数不常用,但在某些场景下却能发挥不寻常的作用。内置函数可以被“提升”,这意味着它们是独一无二的和有用的。因此,掌握内置函数的用法就成了我们应该点亮的技能。.本文是一篇超详细的学习记录,系统、全面、深入的对这两个函数进行了区分和分析。1、eval的基本使用语法:eval(expression,globals=None,locals=None)它有三个参数,其中expression是字符串类型的表达式或代码对象,用于计算;globals和locals是可选参数,默认值为None。具体来说,expression只能是单个表达式,不支持复杂的代码逻辑,比如赋值操作、循环语句等。(PS:单个表达式并不代表“简单无害”,见下文第4节)globals用于指定运行时的全局命名空间,类型为字典,当前模块内置的命名空间被使用默认。locals指定运行时的本地命名空间,类型为字典,默认使用globals的值。当两者都是默认值时,遵循eval函数执行的范围。值得注意的是,这两个并不代表真正的命名空间,它们只在计算过程中起作用,计算完成后销毁。x=10deffunc():y=20a=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:30x:10y:20b:3x:10y:20c:4可以看出,指定命名空间时,会在对应的命名空间中查找变量。此外,它们的值不会覆盖实际命名空间中的值。2、exec的基本使用语法:exec(object[,globals[,locals]])exec在Python2中是一个语句,但是Python3将其转化为一个函数,就像print一样。exec()与eval()高度相似,三个参数的含义和作用也相似。主要区别在于exec()的第一个参数不是表达式,而是代码块,这意味着两点:一是不能进行表达式求值和返回,二是可以执行复杂的代码逻辑,哪个相对更强大。例如,当在代码块中分配一个新变量时,该变量可能会在函数外部的命名空间中存活。>>>x=1>>>y=exec('x=1+1')>>>print(x)>>>print(y)2None可以看出exec()内外的命名空间是interlinked,变量传递出去,不像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':'Pythoncat','age':18}">>>print(eval(a)){'name':'Pythoncat','age':18}#与eval略有不同>>>a="my_dict={'name':'Pythoncat','age':18}">>>exec(a)>>>print(my_dict){'name':'Pythoncat','age':18}eval()函数的返回值是它的表达式执行结果,在某些情况下,它会是None,比如当表达式是print()语句时,或者一个列表的append()操作,这样一个操作的结果是None,所以eval()的返回值也会是None。>>>result=eval('[].append(2)')>>>print(result)Noneexec()函数的返回值只会是None,与执行语句的结果无关,所以exec()函数不需要赋值。如果执行的语句包含return或yield,它们产生的值将不会在exec函数之外起作用。>>>result=exec('1+1')>>>print(result)None两个函数中的globals和locals参数起到白名单的作用,通过限制命名空间的范围来防止范围数据被滥用。compile()函数编译后的代码对象可以作为eval和exec的第一个参数。compile()也是一个神奇的功能。Paradoxicallocalnamespace:如前所述,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()的参数只是一个string,整个东西是常量,没有实现为代码,所以y还不存在。直到第二次print()被解析时,变量y才第一次出现,但是因为没有完全定义,所以y不会被存储在本地命名空间中。在运行时,exec()函数动态创建一个局部变量y。但是,由于Python的实现机制是“本地命名空间不能在运行时改变”,也就是说此时的y不能成为本地命名空间的成员,当执行print()时,会报错。至于为什么locals()的结果有y,为什么不能代表真正的本地命名空间呢?为什么不能动态修改本地命名空间?如果想在执行完exec()后提取y,可以这样做:z=locals()['y'],但是如果不小心写了下面的代码,就会报错:deffoo():exec('y=1+1')y=locals()['y']print(y)foo()#Error:KeyError:'y'#将变量y改成其他变量不会报错KeyError指到字典中不存在相应的key。本例中声明了y,但是由于循环引用导致无法完成赋值,即key值对应的值是一个无效值,所以如果读取不到就会报错。此示例中还有4个变体。我试图以自洽的方式解释它们,但我尝试了很长时间都没有成功。以后再说吧,等我想通了,我会单独写一篇文章。4、为什么要慎用eval()?许多动态编程语言都有一个eval()函数来做同样的事情,但是,无一例外,人们会告诉你避免使用它。为什么要谨慎使用eval()?主要是出于安全考虑,对于不可信的数据源,eval函数很可能会造成代码注入问题。>>>eval("__import__('os').system('whoami')")desktop-fa4b888\pythoncat>>>eval("__import__('subprocess').getoutput('ls~')")#Result略,内容是当前路径的文件信息。在上面的例子中,我的私人数据被暴露了。更可怕的是,如果将命令改为rm-rf~,会删除当前目录下的所有文件。对于上面的例子,有一种限制性的方式,就是指定全局变量为{'__builtins__':None}或{'__builtins__':{}}。>>>s={'__builtins__':None}>>>eval("__import__('os').system('whoami')",s)#Error:TypeError:'NoneType'objectisnotsubscriptablebuiltinscontainsbuilt-在命名空间中的名字,在控制台中输入dir(builtins),可以找到很多内置函数的名字,异常等属性。默认情况下,eval函数的globals参数会隐式携带__builtins__,即使globals参数是{},所以如果要禁用它,必须显式指定它的值。上面的示例将其映射为None,这意味着eval可用的内置命名空间被限制为None,从而限制了表达式调用内置模块或属性的能力。不过这种方法也不是万无一失的,因为还是有发动攻击的手段的。一位漏洞挖掘大师在他的博客中分享了一个思路,让人大开眼界。核心代码就是下面这句话,你可以试着执行一下看看输出了什么。>>>().__class__.__bases__[0].__subclasses__()这段代码的解释和进一步的使用方法可以参考博客。(地址:www.tuicool.com/articles/je...还有一篇博客,不仅提到了上面例子的方法,还提供了一个新的思路:Warning:Donotexecutethefollowingcodeat你自己的风险。>>>eval('(lambdafc=(lambdan:[c1="c"2="in"3="().__class__.__bases__[0"language="for"][/c].__subclasses__()如果c.__name__==n][0]):fc("函数")(fc("代码")(0,0,0,0,"KABOOM",(),(),(),"","",0,""),{})())()',{"__builtins__":None})这行代码会直接导致Python崩溃,具体分析在:segmentfault.com/a/119000001...除了黑客的手段,简单的内容也可以发起攻击,像下面这样写,会在短时间内耗尽服务器的计算资源。>>>eval("2**888888888",{"__builtins__":None},{})如上所述,我们已经直观地论证了eval()函数的危险性。但是,即使Python高手谨慎使用,也不能保证不会出错了。在官方的dumbdbm模块,有(20142019)发现了一个安全漏洞,攻击者可以通过伪造数据库文件调用eval()发起攻击。(详情:bugs.python.org/issue22885)巧合的是,上个月(2019.02),一些针对Python3.8的核心开发者也提出了一个安全问题,建议不要在logging.config中使用eval()函数,问题依旧打开。(详情:bugs.python.org/issue36022)这些足以说明为什么要谨慎使用eval()。出于同样的原因,exec()函数也必须谨慎使用。5.安全替代用法既然存在各种安全隐患,为什么要创建这两个内置方法呢?为什么要使用它们?原因很简单,因为Python是一种灵活的动态语言。与静态语言不同,动态语言支持动态代码生成。对于已经部署的项目,只需要局部的小改动就可以实现bug修复。那么有什么方法可以比较安全地使用它们呢?ast模块的literal()是eval()的安全替代品。与不检查就执行的eval()不同,ast.literal()首先检查表达式的内容是否有效和合法。它允许的文字内容如下:strings,bytes,numbers,tuples,lists,dicts,sets,booleans,None一旦内容不合法就会报错:importastast.literal_eval("__import__('os').system('whoami')")报错:ValueError:malformednodeorstring但是,它也有缺点:AST编译器的堆栈深度有限,当解析的字符串内容太大或太复杂时,该程序可能会崩溃。至于exec(),似乎没有类似的替代方案,毕竟它能支持的内容更加复杂多样。最后提个建议:找出它们的区别和操作细节(比如之前的localnamespace内容),谨慎使用,限制可用的namespace,充分验证数据来源。