이 기사의 내용은 Python에서 eval() 및 exec() 함수의 사용 분석에 대한 내용입니다. 필요한 친구가 참고할 수 있기를 바랍니다.
Python은 다양한 내장 유틸리티 함수(Built-in Functions)를 제공하며, 최신 Python 3 공식 문서에는 69개가 나열되어 있습니다.
print(), open() 및 dir()과 같은 대부분의 함수는 우리가 일반적으로 사용합니다. 일부 함수는 일반적으로 사용되지 않지만 특정 시나리오에서는 특별한 역할을 할 수 있습니다. 내장 기능은 "승격"될 수 있습니다. 즉, 고유한 기능이 있고 유용하다는 의미입니다.
그러므로 내장 기능의 사용법을 익히는 것이 우리가 배워야 할 기술이 되었습니다.
1. eval
기본 사용법: eval(expression, globals=None, locals=None)
3개의 매개변수가 있습니다. 여기서 표현식은 문자열 유형 표현식 또는 코드 객체입니다. 계산에 사용됩니다. 전역 및 지역은 선택적 매개변수이며 기본값은 없음입니다.
특히 표현식은 단일 표현식만 가능하며 할당 연산, 루프 문 등과 같은 복잡한 코드 논리를 지원하지 않습니다. (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에서는 이를 함수로 변환합니다. 인쇄와 같습니다. exec()는 eval()과 매우 유사하며 세 매개변수는 유사한 의미와 기능을 갖습니다.
주요 차이점은 exec()의 첫 번째 매개 변수가 표현식이 아니라 코드 블록이라는 것입니다. 이는 두 가지를 의미합니다. 첫째, 표현식을 평가하고 반환할 수 없으며, 둘째, 복잡한 코드 논리를 실행할 수 있습니다. 예를 들어, 코드 블록에 새 변수가 할당되면 해당 변수는 함수 외부의 네임스페이스에 남아 있을 수 있습니다.
>>> x = 1 >>> y = exec('x = 1 + 1') >>> print(x) >>> print(y) 2 None
문자열 기반 이벤트이며 많은 의미가 있습니다. 그러나 실제 사용해보면 제가 알고 있는 몇 가지 작은 세부 사항이 있습니다.
일반적인 용도: 문자열을 목록으로, 문자열을 dict로, 문자열을 튜플로로 등 문자열을 해당 객체로 변환합니다.>>> 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}
>>> result = eval('[].append(2)') >>> print(result) None
>>> result = exec('1 + 1') >>> print(result) None
def foo(): exec('y = 1 + 1\nprint(y)') print(locals()) print(y) foo()
2 {'y': 2} Traceback (most recent call last): ...(略去部分报错信息) print(y) NameError: name 'y' is not defined
原因与 Python 的编译器有关,对于以上代码,编译器会先将 foo 函数解析成一个 ast(抽象语法树),然后将所有变量节点存入栈中,此时 exec() 的参数只是一个字符串,整个就是常量,并没有作为代码执行,因此 y 还不存在。直到解析第二个 print() 时,此时第一次出现变量 y ,但因为没有完整的定义,所以 y 不会被存入局部命名空间。
在运行期,exec() 函数动态地创建了局部变量 y ,然而由于 Python 的实现机制是“运行期的局部命名空间不可改变 ”,也就是说这时的 y 始终无法成为局部命名空间的一员,当执行 print() 时也就报错了。
至于为什么 locals() 取出的结果有 y,为什么它不能代表真正的局部命名空间?为什么局部命名空间无法被动态修改?可以查看我之前分享的《Python 动态赋值的陷阱》,另外,官方的 bug 网站中也有对此问题的讨论,查看地址:https://bugs.python.org/issue...
若想把 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__()
关于这句代码的解释,以及更进一步的利用手段,详见。(地址:http://www.php.cn/python-tutorials-416494.html)
另外还有一篇博客,不仅提到了上例的手段,还提供了一种新的思路:
#警告:千万不要执行如下代码,后果自负。 >>> 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 掉。具体分析在:http://www.php.cn/python-tutorials-416495.html
除了黑客的手段,简单的内容也能发起攻击。像下例这样的写法, 将在短时间内耗尽服务器的计算资源。
>>> eval("2 ** 888888888", {"__builtins__":None}, {})
如上所述,我们直观地展示了 eval() 函数的危害性,然而,即使是 Python 高手们小心谨慎地使用,也不能保证不出错。
在官方的 dumbdbm 模块中,曾经(2014年)发现一个安全漏洞,攻击者通过伪造数据库文件,可以在调用 eval() 时发起攻击。(详情:https://bugs.python.org/issue...)
无独有偶,在上个月(2019.02),有核心开发者针对 Python 3.8 也提出了一个安全问题,提议不在 logging.config 中使用 eval() 函数,目前该问题还是 open 状态。(详情:https://bugs.python.org/issue...)
如此种种,足以说明为什么要慎用 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() ,似乎还没有类似的替代方法,毕竟它本身可支持的内容是更加复杂多样的。
最后是一个建议:搞清楚它们的区别与运行细节(例如前面的局部命名空间内容),谨慎使用,限制可用的命名空间,对数据源作充分校验。
本篇文章到这里就已经全部结束了,更多其他精彩内容可以关注PHP中文网的python视频教程栏目!
위 내용은 Python의 eval() 및 exec() 함수 사용 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!