Python无疑是一种流行的语言。它年复一年地在最受欢迎和最受喜爱的语言中排名。这并不难解释,考虑到它的流畅性和表现力。其类似于伪代码的语法使初学者非常容易将其作为他们的第一门语言,而其庞大的包库(包括像Django和TensorFlow这样的巨头)确保它可以扩展到任何需要的任务。
作为这样一种广泛使用的语言,Python对恶意黑客来说是一个非常有吸引力的目标。让我们看看几个简单的方法来保护你的Python应用程序,并使黑帽子远离。
问题和解决方案
Python非常重视 "禅",即开发者的快乐。最明显的证据是,Python的指导原则被总结成一首诗。试着在Python外壳中import this 来阅读它。下面是一些可能会扰乱你的禅的安全问题,以及将其恢复到平静状态的解决方案。
不安全的反序列化
OWASP Top Ten,一个网络安全的基本检查表,提到不安全的反序列化是十个最常见的安全缺陷之一。虽然众所周知,执行来自用户的任何东西都是一个糟糕的主意,但序列化和反序列化用户输入似乎并不同样严重。毕竟,没有代码被运行,对吗?错了。
PyYAML是Python中YAML序列化和反序列化的事实标准。该库支持将自定义数据类型序列化为YAML,并将其反序列化为Python对象。请看这里的序列化代码和由它产生的YAML。
!!python/object:__main__.Person
age: 24
name: Dhruv
对这个YAML进行反序列化,可以得到原始数据类型。
$ python deserialize.py ↵
正如你所看到的,YAML中的一行!!python/object:__main__.Person ,描述了如何从它们的文本表现形式中重新确定对象。但这开启了一连串的攻击媒介,当这种实例化可以执行代码时,就会升级为RCE。
解决方案
这个解决方案,虽然看起来很微不足道,但它是通过更换加载器yaml.Loader ,而使用安全加载器yaml.SafeLoader 。这个加载器是比较安全的,因为它完全阻止了自定义类的加载。
$ python deserialize.py ↵
ConstructorError: could not determine a constructor for the tag 'tag:yaml.org,2002:python/object:__main__.Person'
in "person.yml", line 1, column 1
像哈希和数组这样的标准类型仍然可以像以前一样被序列化到YAML文档中并从其中反序列化。大多数人,可能包括你,甚至不会意识到其中的区别。
$ python deserialize.py ↵
{'age': 24, 'name': 'Dhruv'}
动态执行
Python有一对非常危险的函数:exec 和eval 。这两个函数的工作非常相似:将传递给它们的字符串作为Python代码进行处理。exec 希望字符串是一个语句,它将执行这个语句而不返回一个值。eval 希望字符串是一个表达式,将返回这个表达式的计算值。
下面是这两个函数的一个操作例子。
理论上,你可以向eval 传递一个语句,得到与exec 相似的效果,因为在 Python 中,返回None 几乎等同于什么都不返回。
这些函数的危险在于这些函数能够在同一个Python过程中执行几乎所有的代码。向函数传递任何你不能100%确定的输入,就相当于把你的服务器钥匙交给了恶意的黑客。这正是RCE的定义。
解决方案
有一些方法可以减少eval的访问。你可以通过将字典分别作为第二个和第三个参数传递给eval 来限制对 globals 和 locals 的访问。记住,在发生冲突时,locals 比 globals 优先。
这使得代码更加安全,是的。至少它在一定程度上防止了变量中的数据被泄露。

但它仍然不能防止字符串访问任何内置的插件,比如pow ,或者更危险的是__import__ 。为了解决这个问题,你需要覆盖__builtins__ 。
现在可以安全暴露了吗?不完全是。因为不管你把eval 做得多么安全,它的工作就是评估一个表达式,没有什么可以阻止表达式花费太长的时间,使服务器冻结很长时间。
正如我们 Python 开发者喜欢说的,"eval 是邪恶的。"
依赖性管理
Python的流行吸引了白帽安全研究人员的注意,就像吸引了具有恶意的黑客的注意一样。因此,新的安全漏洞不断被发现、披露和修补。为了阻止恶意黑客,你的软件需要保持其所有的依赖关系是最新的。
在Python中钉住软件包的常用技术是无处不在的requirements.txt 文件,这是一个简单的文件,它列出了你的项目所需的所有依赖项和确切的版本。
比方说,你安装了 Django。截至目前,Django还依赖三个软件包。如果你冻结你的依赖关系,你最终会得到以下要求。注意,这些依赖中只有一个是你安装的,其他3个是子依赖。
$ pip freeze ↵
asgiref==3.3.1
Django==3.1.5
pytz==2020.5
sqlparse==0.4.1
pip freeze 不把依赖关系放在级别中,这是个问题。对于只有几个依赖关系的小项目,你可以在精神上保持跟踪,这不是一个大问题,但随着你的项目增长,你的顶级依赖关系也会增长。当子依赖关系重叠时就会产生冲突。更新个别的依赖关系也是一团糟,因为这些依赖关系的图形关系在纯文本文件中并不清楚。
解决方案
Pipenv和Poetry是两个可以帮助你更好地管理依赖关系的工具。我更喜欢Pipenv,但Poetry也同样出色。这两个软件包管理器都建立在pip 。有趣的是: DeepSource与Pipenv和Poetry这两个软件包管理器兼容。
例如,Pipenv在Pipfile 中跟踪你的顶级依赖关系,然后在一个名为Pipfile.lock 的锁文件中做锁定依赖关系的艰苦工作,类似于npm 管理 Node.js 包的方式。这里有一个例子Pipfile 。
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
django = "*"
[requires]
python_version = "3.9"
有了这个,你可以清楚地了解你的应用程序的顶层依赖关系。更新依赖关系也更容易,因为你只需要更新顶层的包,锁定算法会找出所有子依赖关系的最兼容和最新的版本。
下面是使用Django的同一个例子。注意Pipenv如何识别你的顶层依赖和它的依赖等等。
$ pipenv graph ↵
Django==3.1.5
- asgiref [required: >=3.2.10,<4, installed: 3.3.1]
- pytz [required: Any, installed: 2020.5]
- sqlparse [required: >=0.2.2, installed: 0.4.1]
如果你的代码托管在GitHub上,确保你也打开并配置Dependabot。这是一个可爱的小机器人,如果你的任何一个依赖项已经过期,或者某个依赖项的钉子版被发现有漏洞,它会提醒你。Dependabot也会向你的 repo做PR,自动更新你的软件包。确实非常方便!
Python 有一个特殊的assert 关键字,用于防范意外情况。assert 的目的很简单,验证一个条件,如果条件不满足则引发错误。实质上,assert 对给定的表达式进行评估,并且
- 如果它被评估为一个真值,则继续前进
- 如果它的值是错误的,则引发一个
AssertionError,并给出信息
考虑一下这个例子。
def do_something_dangerous(user, command):
assert user.has_permissions(command), f'{user} is not authorized'
user.execute(command)
这是一个非常简单的例子,我们检查用户是否有权限执行一个动作,随后执行给定的动作。在这种情况下,如果user.has_permissions() ,返回False ,我们的断言将引起一个AssertionError ,执行将被停止。看上去很安全,对吗?

不,断言是开发人员在开发和调试阶段的工具。断言不应该被用来保护关键功能。Python常量,__debug__ 在编译过程中被设置为False ,它从编译的代码中移除断言语句,以优化代码的性能。从编译后的代码中删除断言语句会使函数不受保护。
避免使用asserts的另一个原因是,断言错误对调试人员没有帮助,因为它们除了提供断言不成立的事实外,没有其他信息。定义一个合适的异常类,然后提高它们的实例是一个更可靠的解决方案。
解决方案
对于另一种方法,请回到基础。下面是同一个程序,这次使用if/else 并在所需的断言未得到满足时引发一个PermissionError (你需要在某个地方定义它)。
这段代码使用了简单明了的Python结构,在__debug__ 设置为True 或False 的情况下也是如此,并且引发了清晰的异常,可以更清晰地处理。
实现禅意
从所有这些例子中得到的关键启示是,永远不要相信你的用户。用户提供的输入不应该被序列化-反序列化、评估、执行或渲染。为了安全起见,你必须小心你所写的东西,并在代码写完后对其进行彻底审计。
你知道有什么比在代码写完后扫描它的漏洞更好吗?在你写出有漏洞的代码后,立即得到漏洞的提示。
![]()
Bandit
静态代码分析工具,如linters和漏洞扫描器,可以帮助你在它们被野外利用之前发现很多问题。在Python中寻找安全漏洞的一个优秀工具是Bandit。Bandit检查每个文件,为其生成一个抽象语法树(AST),然后在这个AST上运行一系列的测试。Bandit可以检测出一大堆开箱即用的漏洞,还可以针对特定场景进行扩展,并通过插件与框架兼容。事实上,Bandit能够检测到上述所有的安全缺陷。
如果你是一个Python开发者,我不能不推荐Bandit。我可以写一整篇文章来赞美它的优点。我甚至会这么做。
你也应该考虑使用像DeepSource这样的代码审查自动化工具来自动化整个审计和审查过程,它通过它的linters和安全分析器来扫描你的代码,在每一次提交和每一次PR上,并能自动修复大量的问题。DeepSource也有自己的定制分析器,适用于大多数语言,并不断改进和保持最新。而且,它的设置令人难以置信地简单!谁知道它会如此简单?
version = 1
[[analyzers]]
name = "python"
enabled = true
[analyzers.meta]
runtime_version = "3.x.x"
max_line_length = 80
谁知道它可以如此简单?
体验一下Python的禅意吧,小心别让黑帽子打扰你的安宁!