深度辨析Python的eval()与exec():用法、陷阱与安全防御

Ivy
Ivy
发布时间:2026-10-13 | 1人浏览过
原创

本文系统辨析Python内置函数eval()与exec()的核心差异。eval仅处理单个表达式,exec支持代码块;两者在命名空间传递、返回值机制上存在显著不同。重点解析exec在局部变量作用域中的“吊诡”行为及编译器机制,并深入探讨eval()带来的代码注入风险,包括绕过__builtins__限制的攻击手法及资源耗尽攻击,最后推荐ast.literal_eval作为安全替代方案。

Python内置函数概览

Python提供了丰富的内置函数(Built-in Functions),在最新的Python 3官方文档中,共列出了69个此类函数。

其中大部分函数如print()、open()和dir()是我们日常高频使用的,但也有一些函数虽不常露面,却在特定场景下展现出独特价值。能被列为内置函数,说明它们各自具备不可替代的作用。

因此,熟练掌握这些内置函数的用法,是Python开发者必须点亮的关键技能。

在之前的文章《Python进阶:如何将字符串常量转为变量?》中,我曾提及eval()和exec(),但并未深入展开。为了弥补这一知识盲区,我重新梳理了相关资料。本文是一份详尽的学习笔记,旨在系统、全面且深入地辨析这两个重要函数。

eval()与exec()的基本用法辨析

1、eval的基本用法

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

该函数包含三个参数。其中expression是必需的,类型为字符串表达式或代码对象,用于执行运算;globals和locals是可选参数,默认值为None。

具体而言,expression只能包含单个表达式,不支持复杂的代码逻辑(如赋值语句、循环等)。需要注意的是,“单个表达式”并不等同于“简单无害”,具体风险见下文第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]])

在Python 2中,exec是一个语句,而在Python 3中被改造为函数,类似于print()。exec()与eval()在语法结构上高度相似,三个参数的意义和作用也相近。

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


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

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

执行细节与局部命名空间陷阱

3、一些细节辨析

这两个函数都非常强大,它们将字符串内容视为有效代码执行。这是一种由字符串驱动的事件机制,意义重大。然而,在实际使用中,存在许多细微的细节,此处列出我所知的一些要点。

常见用途包括将字符串转换为相应的对象,例如将string转为list、dict或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参数起到了白名单的作用,通过限定命名空间的范围,防止作用域内的数据被滥用。

compile()函数编译后的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个变种,我想用一套自洽的说法来解释它们,但尝试了很久,未果。留个后话吧,等我想明白,再单独写一篇文章。

eval()的安全风险与防御机制

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://www.jb51.net/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://www.jb51.net/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

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 相关产品和应用

全部下载

专题列表