Python의 eval() 및 exec() 함수 사용 분석

不言
풀어 주다: 2019-03-25 10:45:01
앞으로
3106명이 탐색했습니다.

이 기사의 내용은 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
로그인 후 복사
함수의 실행 결과를 받기 위해 변수가 필요한 eval() 함수와 달리 exec() 내부와 외부의 네임스페이스가 연결되어 있고, 여기에서 변수가 전달되는 것을 볼 수 있습니다.

3. 몇 가지 자세한 분석

두 함수 모두 문자열 내용을 유효한 코드로 실행합니다. 이것은

문자열 기반 이벤트이며 많은 의미가 있습니다. 그러나 실제 사용해보면 제가 알고 있는 몇 가지 작은 세부 사항이 있습니다.

일반적인 용도: 문자열을 목록으로, 문자열을 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}
로그인 후 복사
eval() 함수의 반환 값은 표현식의 실행 결과입니다. 표현식이 print() 문이거나 목록의 추가() 작업인 경우와 같이 None이 됩니다. , this 클래스 작업의 결과는 None이므로 eval()의 반환 값도 None이 됩니다.

>>> result = eval('[].append(2)')
>>> print(result)
None
로그인 후 복사
exec() 함수의 반환 값은 None만 되며, 이는 실행 문의 결과와 관련이 없습니다. 따라서 exec() 함수에 값을 할당할 필요가 없습니다. 실행된 문에 return 또는 항복이 포함된 경우 생성된 값은 exec 함수 외부에서 사용할 수 없습니다.

>>> result = exec('1 + 1')
>>> print(result)
None
로그인 후 복사
두 함수의 globals 및 locals 매개변수는 화이트리스트 역할을 하여 네임스페이스 범위를 제한하여 범위 내의 데이터가 남용되는 것을 방지합니다.

conpile() 함수의 컴파일된 코드 개체는 eval 및 exec의 첫 번째 매개 변수로 사용할 수 있습니다. compile()도 마법의 함수입니다. 제가 번역한 마지막 기사인 "Python Sassy Operations: Dynamicly Define Functions"에서는 동적으로 함수를 정의하는 작업을 보여주었습니다.

역설적인 로컬 네임스페이스: 앞서 언급했듯이 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/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 중국어 웹사이트의 기타 관련 기사를 참조하세요!

관련 라벨:
원천:segmentfault.com
본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.
인기 튜토리얼
더>
최신 다운로드
더>
웹 효과
웹사이트 소스 코드
웹사이트 자료
프론트엔드 템플릿