Python 全栈安全(三)(3)

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: Python 全栈安全(三)

Python 全栈安全(三)(2)https://developer.aliyun.com/article/1508747

13.2 YAML 远程代码执行

在第七章,你看到 Mallory 进行远程代码执行攻击。首先,她将恶意代码嵌入到一个 pickled,或者序列化的 Python 对象中。接下来,她将这段代码伪装成基于 cookie 的 HTTP 会话状态并发送给服务器。服务器在不知不觉中使用 PickleSerializer,Python 的 pickle 模块的包装器,执行了恶意代码。在本节中,我将展示如何使用 YAML 而不是 pickle 进行类似的攻击——相同的攻击,不同的数据格式。

注意 在撰写本文时,不安全的反序列化在 OWASP 十大漏洞中排名第 8 位 (owasp.org/www-project-top-ten/)。

像 JSON、CSV 和 XML 一样,YAML 是一种用人类可读的格式表示数据的常见方式。每种主要的编程语言都有工具来解析、序列化和反序列化这些格式的数据。Python 程序员通常使用 PyYAML 来解析 YAML。在您的虚拟环境中,运行以下命令安装 PyYAML:

$ pipenv install pyyaml

打开一个交互式 Python shell 并运行以下代码。这个例子将一个小的内联 YAML 文档传递给 PyYAML。如粗体显示,PyYAML 使用BaseLoader加载文档并将其转换为 Python 字典:

>>> import yaml
>>> 
>>> document = """                             # ❶
...   title: Full Stack Python Security        # ❶
...   characters:                              # ❶
...     - Alice                                # ❶
...     - Bob                                  # ❶
...     - Charlie                              # ❶
...     - Eve                                  # ❶
...     - Mallory                              # ❶
... """                                        # ❶
>>> 
>>> book = yaml.load(document, Loader=yaml.BaseLoader)
>>> book['title']                              # ❷
'Full Stack Python Security'                   # ❷
>>> book['characters']                         # ❷
['Alice', 'Bob', 'Charlie', 'Eve', 'Mallory']  # ❷

❶ 从 YAML . . .

❷ . . . 到 Python

在第一章中,你学到了最小权限原则。PLP 表明用户或系统应该只被赋予执行其职责所需的最小权限。我向你展示了如何将这个原则应用到用户授权上;这里我将向你展示如何将其应用到解析 YAML 上。

警告 当你将 YAML 加载到内存中时,限制你给予 PyYAML 的权限非常重要。

你可以通过 Loader 关键字参数将 PLP 应用到 PyYAML。例如,前面的例子使用了最不强大的加载器 BaseLoader 加载了 YAML。PyYAML 支持其他三种 Loader。以下从最不强大到最强大列出了这四种 Loader。每个 Loader 支持的功能更多,风险也更大。

  • BaseLoader—支持原始的 Python 对象,如字符串和列表
  • SafeLoader—支持原始的 Python 对象和标准 YAML 标签
  • FullLoader—完整的 YAML 语言支持(默认)
  • UnsafeLoader—完整的 YAML 语言支持和任意函数调用

如果你的系统接受 YAML 作为输入,不遵循 PLP 可能是致命的。以下代码演示了当使用 UnsafeLoader 从不受信任的源加载 YAML 时会有多么危险。此示例创建了一个内联 YAML,其中嵌入了对 sys.exit 的函数调用。如粗体字所示,然后将 YAML 输入给 PyYAML。然后,该过程使用退出码 42 调用 sys.exit 杀死自身。最后,echo 命令结合 $? 变量确认 Python 进程确实以值 42 退出:

$ python                                           # ❶
>>> import yaml
>>> 
>>> input = '!!python/object/new:sys.exit [42]'    # ❷
>>> yaml.load(input, Loader=yaml.UnsafeLoader)     # ❸
$ echo $?                                          # ❹
42                                                 # ❹

❶ 创建进程

❷ 内联 YAML

❸ 杀死进程

❹ 确认死亡

你很可能永远不会需要以这种方式调用函数来进行商业用途。你不需要这个功能,那么为什么要冒险呢?BaseLoaderSafeLoader 是从不受信任的源加载 YAML 的推荐方式。或者,调用 yaml.safe_load 相当于使用 SafeLoader 调用 yaml.load

警告 PyYAML 的不同版本默认使用不同的 Loader,所以你应该始终明确指定你需要的 Loader。调用 yaml.load 而不带 Loader 关键字参数已经被弃用。

在调用 load 方法时,始终指定 Loader。如果不这样做,可能会使您的系统在运行较旧版本的 PyYAML 时变得脆弱。直到版本 5.1,默认的 Loader 是(相当于)UnsafeLoader;当前的默认 LoaderFullLoader。我建议避免使用这两种。

保持简单

我们在撰写本文时,即使是 PyYAML 的网站(github.com/yaml/pyyaml/wiki/PyYAML-yaml.load(input)-Deprecation)也不推荐使用 FullLoader

目前应该避免使用 FullLoader 加载器类。2020 年 7 月在 5.3.1 版本中发现了新的漏洞。这些漏洞将在下一个版本中解决,但如果发现更多的漏洞,那么 FullLoader 可能会消失。

在下一节中,我将继续使用不同的数据格式 XML 进行注入攻击。XML 不仅令人讨厌;我认为您会对它有多危险感到惊讶。

13.3 XML 实体扩展

在这一节中,我讨论了一些旨在耗尽系统内存的攻击。这些攻击利用了一个鲜为人知的 XML 功能,称为实体扩展。什么是 XML 实体?实体声明允许您在 XML 文档中定义和命名任意数据。实体引用是一个占位符,允许您在 XML 文档中嵌入一个实体。XML 解析器的工作是将实体引用扩展为实体。

将以下代码键入交互式 Python shell 中作为一个具体的练习。这段代码以粗体字显示一个小的内联 XML 文档开头。在这个文档中只有一个实体声明,代表文本Alice。根元素两次引用这个实体。在解析文档时,每个引用都会被扩展,将实体嵌入两次:

>>> from xml.etree.ElementTree import fromstring
>>> 
>>> xml = """                 # ❶
... <!DOCTYPE example [
...   <!ENTITY a "Alice">     # ❷
... ]>
... <root>&a;&a;</root>       # ❸
... """
>>> 
>>> example = fromstring(xml)
>>> example.text              # ❹
'AliceAlice'                  # ❹

❶ 定义一个内联 XML 文档

❷ 定义一个 XML 实体

❸ 根元素包含三个实体引用。

❹ 实体扩展演示

在这个例子中,一对三个字符的实体引用充当了一个五个字符的 XML 实体的占位符。这并没有以有意义的方式减少文档的总大小,但想象一下如果实体长度为 5000 个字符会怎样。因此,内存保护是 XML 实体扩展的一个应用;在接下来的两节中,您将了解到这个功能是如何被滥用以达到相反的效果。

13.3.1 二次膨胀攻击

攻击者通过武器化 XML 实体扩展来执行二次膨胀攻击。考虑以下代码。这个文档包含一个只有 42 个字符长的实体;这个实体只被引用了 10 次。二次膨胀攻击利用了一个像这样的文档,其中实体和引用计数的数量级更大。数学并不困难;例如,如果实体是 1 MB,实体被引用了 1024 次,那么文档的大小将约为 1 GB:

<!DOCTYPE bomb [
  <!ENTITY e "a loooooooooooooooooooooooooong entity ...">   # ❶
]>
<bomb>&e;&e;&e;&e;&e;&e;&e;&e;&e;&e;</bomb>                  # ❷

❶ 单个实体声明

❷ 10 个实体引用

输入验证不足的系统很容易成为二次膨胀攻击的目标。攻击者注入少量数据;系统随后超出其内存容量,试图扩展数据。因此,恶意输入被称为内存炸弹。在下一节中,我将向您展示一个更大的内存炸弹,并教您如何化解它。

13.3.2 十亿笑攻击

这种攻击很有趣。十亿笑攻击,也被称为指数级膨胀扩展攻击,类似于二次膨胀攻击,但效果更加显著。这种攻击利用了 XML 实体可能包含对其他实体的引用的事实。很难想象在现实世界中有商业用途的情况下会使用这个功能。

以下代码示例说明了如何执行十亿笑话攻击。此文档的根元素仅包含一个实体引用,以粗体显示。此引用是实体嵌套层次结构的占位符:

<!DOCTYPE bomb [
  <!ENTITY a "lol">                               # ❶
  <!ENTITY b "&a;&a;&a;&a;&a;&a;&a;&a;&a;&a;">    # ❶
  <!ENTITY c "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">    # ❶
  <!ENTITY d "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">    # ❶
]>
<bomb>&d;</bomb>

❶ 四个嵌套层次的实体

处理此文档将强制 XML 解析器将此引用展开为文本 lol 的仅 1000 个重复。一个十亿笑话攻击利用了这样一个具有更多层次嵌套实体的 XML 文档。每个级别将内存消耗增加一个数量级。这种技术将使用不超过本书一页的 XML 文档超出任何计算机的内存容量。

像大多数编程语言一样,Python 有许多解析 XML 的 API。minidompulldomsaxetree 包都容易受到二次增长和十亿笑话攻击的影响。为了保护 Python,这些 API 只是遵循 XML 规范。

显然,向系统添加内存并不是解决此问题的方法;添加输入验证是。Python 程序员通过名为 defusedxml 的库来抵御内存炸弹。在您的虚拟环境中,运行以下命令来安装 defusedxml

$ pipenv install defusedxml

defusedxml 库旨在成为 Python 原生 XML API 的一个即插即用替代品。例如,让我们比较两个代码块。以下代码将使系统崩溃,因为它试图解析恶意 XML:

from xml.etree.ElementTree import parse
parse('/path/to/billion_laughs.xml')    # ❶

❶ 打开了一个内存炸弹

相反,以下代码在尝试解析相同文件时会引发 EntitiesForbidden 异常。唯一的区别是 import 语句:

from xml.etree.ElementTree import parse
from defusedxml.ElementTree import parse
parse('/path/to/billion_laughs.xml')    # ❶

❶ 引发一个 EntitiesForbidden 异常

在底层,defusedxml 封装了每个 Python 原生 XML API 的 parse 函数。defusedxml 定义的 parse 函数默认不支持实体展开。如果您需要在从受信任的来源解析 XML 时使用此功能,可以自由使用 forbid_entities 关键字参数覆盖此行为。表 13.1 列出了 Python 的每个原生 XML API 及其相应的 defusedxml 替代品。

表 13.1 Python XML API 和 defusedxml 替代方案

原生 Python API defusedxml API
from xml.dom.minidom import parse from defusedxml.minidom import parse
from xml.dom.pulldom import parse from defusedxml.pulldom import parse
from xml.sax import parse from defusedxml.sax import parse
from xml.etree.ElementTree import parse from defusedxml.ElementTree import parse

本章提出的内存炸弹既是注入攻击又是 拒绝服务 (DoS) 攻击。在下一节中,您将学习如何识别和抵御其他几种 DoS 攻击。

13.4 拒绝服务

你可能已经熟悉 DoS 攻击了。这些攻击旨在通过消耗过多的资源来压倒系统。DoS 攻击的目标资源包括内存、存储空间、网络带宽和 CPU。DoS 攻击的目标是通过损害系统的可用性来阻止用户访问服务。DoS 攻击有无数种方式进行。最常见的 DoS 攻击形式是通过向系统发送大量恶意网络流量来实施。

DoS 攻击计划通常比仅仅向系统发送大量网络流量更加复杂。最有效的攻击会操纵流量的特定属性,以增加对目标的压力。许多这些攻击利用了格式错误的网络流量,以利用低级网络协议实现。像 NGINX 这样的 Web 服务器,或者像 AWS 弹性负载均衡这样的负载均衡解决方案,是抵御这些攻击的合适场所。另一方面,像 Django 这样的应用服务器,或者像 Gunicorn 这样的 Web 服务器网关接口,则不适合这项工作。换句话说,这些问题不能用 Python 解决。

在本节中,我专注于更高级的基于 HTTP 的 DoS 攻击。相反,负载均衡器和 Web 服务器是抵御这些攻击的错误场所;应用服务器和 Web 服务器网关接口才是正确的场所。表 13.2 说明了一些 Django 设置,您可以使用这些设置来配置这些属性的限制。

表 13.2 Django 抗 DoS 攻击设置

设置 描述
DATA_UPLOAD_MAX_NUMBER_FIELDS 配置允许的请求参数最大数量。如果此检查失败,Django 将引发 SuspiciousOperation 异常。此设置默认为 1000,但合法的 HTTP 请求很少有这么多字段。
DATA_UPLOAD_MAX_MEMORY_SIZE 限制请求体的最大大小(以字节为单位)。此检查忽略文件上传数据。如果请求体超过此限制,Django 将引发 Suspicious-Operation 异常。
FILE_UPLOAD_MAX_MEMORY_SIZE 表示上传到内存中的文件在写入磁盘之前的最大大小(以字节为单位)。此设置旨在限制内存消耗;它不限制上传文件的大小。

警告 上一次你见到有 1000 个字段的表单是什么时候?将 DATA_UPLOAD_MAX_NUMBER_FIELDS 从 1000 减少到 50 或许值得您的时间。

DATA_UPLOAD_MAX_MEMORY_SIZEFILE_UPLOAD_MAX_MEMORY_SIZE 合理地默认为 2,621,440 字节(2.5 MB)。将这些设置分配给 None 将禁用该检查。

表 13.3 说明了一些 Gunicorn 参数,用于抵御其他几种基于 HTTP 的 DoS 攻击。

表 13.3 Gunicorn 抗 DoS 攻击参数

参数 描述
limit-request-line 表示请求行的大小限制,以字节为单位。请求行包括 HTTP 方法,协议版本和 URL。URL 是明显的限制因素。此设置默认为 4094;最大值为 8190。将其设置为 0 将禁用检查。
limit-request-fields 限制请求允许具有的 HTTP 头数。此设置限制的“字段”不是表单字段。默认值合理设置为 100。limit-request-fields 的最大值为 32768。
limit-request-field_size 表示 HTTP 头的最大允许大小。下划线不是打字错误。默认值为 8190。将其设置为 0 允许无限大小的头。Web 服务器通常也执行此检查。

本节的主要观点是,HTTP 请求的任何属性都可以被武器化;这包括大小、URL 长度、字段计数、字段大小、文件上传大小、头计数和头大小。在下一节中,您将了解到由单个请求头驱动的攻击。

13.5 主机头攻击

在我们深入讨论Host头攻击之前,我将解释为什么浏览器和 Web 服务器使用Host头。Web 服务器在网站和其用户之间中继 HTTP 流量。Web 服务器经常为多个网站执行此操作。在这种情况下,Web 服务器将每个请求转发到浏览器设置Host头的任何网站。这样可以防止将 alice.com 的流量发送到 bob.com,反之亦然。图 13.3 说明了一个 Web 服务器在两个用户和两个网站之间路由 HTTP 请求的情况。


图 13.3 一个 Web 服务器使用主机头来在 Alice 和 Bob 之间路由 Web 流量。

Web 服务器通常配置为将缺少或无效的Host头的请求转发到默认网站。如果此网站盲目信任Host头值,它将变得容易受到Host头攻击的影响。

假设 Mallory 向 alice.com 发送密码重置请求。她伪造了Host头值,将其设置为mallory.com而不是alice.com。她还将电子邮件地址字段设置为bob@bob.com而不是mallory@mallory.com

Alice 的 Web 服务器收到 Mallory 的恶意请求。不幸的是,Alice 的 Web 服务器配置为将包含无效Host头的请求转发到她的应用服务器。应用服务器接收到密码重置请求并向 Bob 发送密码重置电子邮件。就像你在第九章学习发送的密码重置电子邮件一样,发送给 Bob 的电子邮件包含一个密码重置链接。

Alice 的应用程序服务器如何生成 Bob 的密码重置链接?不幸的是,它使用了传入的 Host 头。这意味着 Bob 收到的 URL 是针对 mallory.com 而不是 alice.com 的;此链接还包含密码重置令牌作为查询参数。Bob 打开电子邮件,点击链接,不小心将密码重置令牌发送到 mallory.com。然后,Mallory 使用密码重置令牌重置了 Bob 的密码,并接管了 Bob 的帐户。图 13.4 描绘了这种攻击。


图 13.4 Mallory 利用 Host 头攻击接管了 Bob 的帐户。

您的应用程序服务器永远不应从客户端获取其标识。因此,直接访问 Host 头是不安全的,像这样:

bad_practice = request.META['HTTP_HOST']    # ❶

❶ 绕过输入验证

如果需要访问主机名,请始终在请求上使用 get_host 方法。此方法验证并检索 Host 头:

good_practice = request.get_host()    # ❶

❶ 验证 Host 头

get_host 方法如何验证 Host 头?通过根据 ALLOWED_HOSTS 设置对其进行验证。该设置是允许应用程序提供资源的主机和域名列表。默认值为空列表。如果 DEBUG 设置为 True,Django 允许使用 localhost127.0.0.1[::1]Host 头来方便地进行本地开发。表 13.4 展示了如何为生产环境配置 ALLOWED_HOSTS

表 13.4 ALLOWED_HOSTS 配置示例

示例 描述 匹配 不匹配
alice.com 完全合格的名称 alice.com sub.alice.com
sub.alice.com 完全合格的名称 sub.alice.com alice.com
.alice.com 子域通配符 alice.com,sub.alice.com
* 通配符 alice.com,sub.alice.com,bob.com

警告:不要将 * 添加到 ALLOWED_HOSTS 中。许多程序员出于方便而这样做,他们不知道这实际上是在禁用 Host 头验证。

配置 ALLOWED_HOSTS 的一种方便方法是在应用程序启动时从公钥证书中动态提取主机名。这对于在不同环境中部署具有不同主机名的系统非常有用。清单 13.1 展示了如何使用 cryptography 包执行此操作。此代码打开公钥证书文件,解析它,并将其存储在内存中作为对象。然后,从对象中复制主机名属性到 ALLOWED_HOSTS 设置。

清单 13.1 从公钥证书中提取主机

from cryptography.hazmat.backends import default_backend
from cryptography.x509.oid import NameOID
with open(CERTIFICATE_PATH, 'rb') as f:                            # ❶
    cert = default_backend().load_pem_x509_certificate(f.read())   # ❶
atts = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)    # ❶
ALLOWED_HOSTS = [a.value for a in atts]                            # ❷

❶ 在启动时从证书中提取通用名称

❷ 将常见名称添加到 ALLOWED_HOSTS 中

注意 ALLOWED_HOSTS 与 TLS 无关。像任何其他应用程序服务器一样,Django 在很大程度上不知道 TLS。Django 仅使用 ALLOWED_HOSTS 设置来防止 Host 头攻击。

再次强调,如果可能,攻击者将武器化 HTTP 请求的任何属性。在下一节中,我将介绍攻击者使用的另一种将恶意输入嵌入请求 URL 中的技术。

13.6 开放式重定向攻击

作为开放式重定向攻击主题的介绍,让我们假设 Mallory 想要偷走 Bob 的钱。首先,她冒充 bank.alice.com,使用 bank.mallory.com。Mallory 的网站看起来和感觉就像 Alice 的在线银行网站。接下来,Mallory 准备了一封设计成看起来像来自 bank.alice.com 的电子邮件。这封电子邮件的正文包含一个指向 bank.mallory.com 登录页面的链接。Mallory 把这封电子邮件发送给 Bob。Bob 点击链接,转到 Mallory 的网站,并输入他的登录凭据。然后 Mallory 的网站使用 Bob 的凭据访问他在 bank.alice.com 的账户。Bob 的钱随后被转移到 Mallory 那里。

通过点击链接,Bob 被认为是钓鱼,因为他上了钩。Mallory 已成功执行了一次钓鱼诈骗。这种诈骗有多种形式:

  • 钓鱼 攻击通过电子邮件到达。
  • Smishing 攻击通过短信服务(SMS)到达。
  • Vishing 攻击通过语音邮件到达。

Mallory 的诈骗直接针对 Bob,Alice 几乎无能为力阻止它。然而,如果她不小心,Alice 实际上会让 Mallory 的事情变得更加轻松。假设 Alice 为 bank.alice.com 添加了一个功能。这个功能动态地将用户重定向到站点的另一部分。bank.alice.com 如何知道将用户重定向到哪里?重定向的地址由请求参数的值确定。(在第八章,您通过相同的机制实现了支持相同功能的身份验证工作流程。)

不幸的是,bank.alice.com 在将用户重定向到地址之前并未验证每个地址。这被称为开放式重定向,使得 bank.alice.com 容易受到开放式重定向攻击的影响。开放式重定向使得 Mallory 更容易发动更有效的钓鱼诈骗。Mallory 利用这个机会给 Charlie 发送一封带有指向开放式重定向的链接的电子邮件。这个 URL 在图 13.5 中显示,指向 bank.alice.com 的域名。


图 13.5 开放式重定向攻击的 URL 结构

在这种情况下,Charlie 更有可能上钩,因为他收到了一个带有他银行主机的 URL。不幸的是对于 Charlie,他的银行将他重定向到了 Mallory 的网站,在那里他输入了他的凭据和个人信息。图 13.6 描述了这种攻击。


图 13.6 Mallory 用开放式重定向攻击钓鱼 Bob。

列表 13.2 描述了一个简单的开放式重定向漏洞。OpenRedirectView 执行一个任务,然后读取查询参数的值。然后用户被盲目地重定向到下一个参数值。

列表 13.2 没有输入验证的开放式重定向

from django.views import View
from django.shortcuts import redirect
class OpenRedirectView(View):
    def get(self, request):
        ...
        next = request.GET.get('next')    # ❶
        return redirect(next)             # ❷

❶ 读取下一个请求参数

❷ 发送重定向响应

相反,在第 13.3 节的ValidatedRedirectView通过输入验证抵抗开放式重定向攻击。这个视图将工作委托给 Django 内置的实用函数url_has_allowed_host_and_scheme。这个函数,以粗体字显示,接受一个 URL 和主机。只有当 URL 的域与主机匹配时,它才返回True

第 13.3 节的抵抗开放式重定向攻击的输入验证

from django.http import HttpResponseBadRequest
from django.utils.http import url_has_allowed_host_and_scheme
class ValidatedRedirectView(View):
    def get(self, request):
        ...
        next = request.GET.get('next')                                     # ❶
        host = request.get_host()                                          # ❷
        if url_has_allowed_host_and_scheme(next, host, require_https=True):# ❸
            return redirect(next)
        return HttpResponseBadRequest()                                    # ❹

❶ 读取下一个请求参数

❷ 安全确定主机

❸ 验证重定向的主机和协议

❹ 防止攻击

ValidatedRedirectView注意到使用get_host方法确定主机名,而不是直接访问Host头。在前一节中,您学会了通过这种方式避免Host头攻击。

在罕见的情况下,您的系统可能实际上需要动态地将用户重定向到多个主机。url_has_allowed_host_and_scheme函数通过接受单个主机名或多个主机名的集合来适应这种用例。

如果require_https关键字参数设置为Trueurl_has_allowed_host_and_scheme函数将拒绝使用 HTTP 的任何 URL。不幸的是,这个关键字参数默认为False,为另一种开放式重定向攻击创造了机会。

假设 Mallory 和 Eve 合作进行攻击。Mallory 通过针对 Charlie 的另一次网络钓鱼诈骗开始这次攻击。Charlie 收到一封包含以下 URL 的电子邮件。请注意,源和目标主机相同;协议以粗体字显示,不同:

https:/./alice.com/open_redirect/?next=http:/./alice.com/resource/

Charlie 点击链接,将他带到 Alice 的站点,通过 HTTPS。不幸的是,Alice 的开放式重定向随后将他发送到站点的另一个部分,通过 HTTP。网络窃听者 Eve 接替 Mallory 继续进行中间人攻击。

警告:require_https的默认值为False。您应该将其设置为True

在下一节中,我将本章结束于可能是最为人熟知的注入攻击。无需介绍。

13.7 SQL 注入

在阅读本书的过程中,您已经实现了支持用户注册、身份验证和密码管理等功能的工作流程。与大多数系统一样,您的项目通过在用户和关系数据库之间来回传递数据来实现这些工作流程。当这样的工作流程未能验证用户输入时,它们就成为SQL 注入的一个向量。

攻击者通过向易受攻击的系统提交恶意 SQL 代码作为输入来进行 SQL 注入。系统试图处理输入,但不慎执行它。这种攻击用于修改现有的 SQL 语句或将任意 SQL 语句注入系统。这使攻击者能够破坏、修改或未经授权地访问数据。

一些安全书籍专门有一整章内容介绍 SQL 注入。本书的少数读者会完整地读完关于这个主题的整个章节,因为像 Python 社区的其他成员一样,你们已经采用了 ORM 框架。ORM 框架不仅为您读写数据;它们还是防止 SQL 注入的一层防线。每个主要的 Python ORM 框架,如 Django ORM 或 SQLAlchemy,都通过自动查询参数化有效地抵抗 SQL 注入。

警告:ORM 框架优于编写原始 SQL。原始 SQL 容易出错,工作量更大,而且难看。

有时,对象关系映射并不是解决问题的正确工具。例如,您的应用程序可能需要执行复杂的 SQL 查询以提高性能。在这些罕见的场景中,当您必须编写原始 SQL 时,Django ORM 支持两个选项:原始 SQL 查询和数据库连接查询。

13.7.1 原始 SQL 查询

每个 Django 模型类都通过名为 objects 的属性引用查询接口。在其他功能中,此接口通过名为 raw 的方法容纳原始 SQL 查询。此方法接受原始 SQL 并返回一组模型实例。以下代码说明了一个可能返回大量行的查询。为了节省资源,仅选择表的两列:

from django.contrib.auth.models import User
sql = 'SELECT id, username FROM auth_user'      # ❶
users_with_username = User.objects.raw(sql)

❶ 为所有行选择两列

假设以下查询旨在控制哪些用户被允许访问敏感信息。按预期,当 first_name 等于 Alice 时,raw 方法返回单个用户模型。不幸的是,Mallory 可以通过操纵 first_name"Alice' OR first_name = 'Mallory" 来提升她的权限:

sql = "SELECT * FROM auth_user WHERE first_name = '%s' " % first_name
users = User.objects.raw(sql)

警告:原始 SQL 和字符串插值是一种可怕的组合。

请注意,在占位符 %s 周围加引号会提供一种虚假的安全感。在占位符周围加引号不会提供任何安全性,因为 Mallory 可以简单地准备包含额外引号的恶意输入。

警告:对占位符加引号不会使原始 SQL 变得安全。

通过调用 raw 方法,您必须负责对查询进行参数化。这样可以通过转义所有特殊字符(如引号)来保护您的查询。以下代码演示了如何通过将参数值列表(以粗体显示)传递给 raw 方法来执行此操作。Django 会遍历这些值,并安全地将它们插入到您的原始 SQL 语句中,转义所有特殊字符。以这种方式准备的 SQL 语句不受 SQL 注入的影响。请注意,占位符周围没有引号:

sql = "SELECT * FROM auth_user WHERE first_name = %s"
users = User.objects.raw(sql, [first_name])

或者,raw 方法接受一个字典而不是一个列表。在这种情况下,raw 方法安全地将 %(dict_key) 替换为字典中 dict_key 映射到的内容。

13.7.2 数据库连接查询

Django 允许你通过数据库连接直接执行任意原始 SQL 查询。如果你的查询不属于单个模型类,或者想要执行UPDATEINSERTDELETE语句,这将非常有用。

连接查询与原始方法查询一样具有很大的风险。例如,假设以下查询旨在删除单个经过身份验证的消息。当msg_id等于42时,此代码会按预期运行。不幸的是,如果 Mallory 能够操纵msg_id42 OR 1 = 1,她将摧毁表中的每条消息:

from django.db import connection
sql = """DELETE FROM messaging_authenticatedmessage    # ❶
         WHERE id = %s """ % msg_id                    # ❶
with connection.cursor() as cursor:                    # ❷
    cursor.execute(sql)                                # ❷

❶ 带有一个占位符的 SQL 语句

❷ 执行 SQL 语句

raw方法查询一样,安全地执行连接查询的唯一方法是使用查询参数化。连接查询的参数化方式与raw方法查询相同。以下示例演示了如何使用params关键字参数安全地删除经过身份验证的消息,关键字参数以粗体显示:

sql = """DELETE FROM messaging_authenticatedmessage
         WHERE id = %s """                 # ❶
with connection.cursor() as cursor:
    cursor.execute(sql, params=[msg_id])   # ❷

❶ 未引用的占位符

❷ 转义特殊字符,执行 SQL 语句

我在本章中涵盖的攻击和对策并不像我在其余章节中涵盖的那么复杂。例如,跨站请求伪造和点击劫持有专门的章节。下一章完全致力于一类攻击,称为跨站脚本。这些攻击比我在本章中介绍的所有攻击更复杂和常见。

摘要

  • 哈希和数据完整性有效地抵抗包注入攻击。
  • 解析 YAML 和解析pickle一样危险。
  • XML 不仅仅是丑陋的;从不受信任的来源解析它可能会导致系统崩溃。
  • 你可以通过你的 Web 服务器和负载均衡器抵抗低级 DoS 攻击。
  • 你可以通过你的 WSGI 或应用服务器抵抗高级 DoS 攻击。
  • 开放重定向攻击会导致网络钓鱼和中间人攻击。
  • 对象关系映射有效地抵抗 SQL 注入。

第十四章:跨站脚本攻击

本章内容包括

  • 使用表单和模型验证输入
  • 使用模板引擎转义特殊字符
  • 使用响应头限制浏览器功能

在前一章中,我向你介绍了几种小型注入攻击。在本章中,我继续介绍一种被称为 跨站脚本XSS)的大家族。XSS 攻击有三种类型:持久型、反射型和基于 DOM 的。这些攻击既常见又强大。

注意:在撰写本文时,XSS 在 OWASP 十大安全威胁中排名第 7 位(owasp.org/www-project-top-ten/)。

XSS 抵御是 深度防御 的一个极好例子;一行防护不够。你将在本章中学习如何通过验证输入、转义输出和管理响应头来抵御 XSS。

14.1 什么是 XSS?

XSS 攻击有多种形式和大小,但它们都有一个共同点:攻击者向另一个用户的浏览器注入恶意代码。恶意代码可以采用多种形式,包括 JavaScript、HTML 和层叠样式表(CSS)。恶意代码可以通过许多途径传送,包括 HTTP 请求的主体、URL 或头部。

XSS 有三个子类别。每个子类别都由用于注入恶意代码的机制定义。

  • 持久型 XSS
  • 反射型 XSS
  • 基于 DOM 的 XSS

在本节中,Mallory 进行了所有三种形式的攻击。Alice、Bob 和 Charlie 都将遭受损失。在后续章节中,我将讨论如何抵御这些攻击。

14.1.1 持久型 XSS

假设 Alice 和 Mallory 是 social.bob.com 的用户,这是一个社交媒体网站。像其他社交媒体网站一样,Bob 的网站允许用户分享内容。不幸的是,这个网站缺乏足够的输入验证;更重要的是,它在不转义的情况下呈现共享内容。Mallory 注意到了这一点,并创建了以下一行脚本,旨在将 Alice 从 social.bob.com 带到冒牌站点 social.mallory.com:

<script>
    document.location = "https:/./social.mallory.com";    # ❶
</script>

❶ 客户端重定向的等效

接下来,Mallory 导航到她的个人资料设置页面。她将她的一个个人资料设置更改为她恶意代码的值。Bob 的网站不验证 Mallory 的输入,而是将其持久化到数据库字段中。

后来,Alice 偶然发现了 Mallory 的个人资料页面,现在包含了 Mallory 的代码。Alice 的浏览器执行了 Mallory 的代码,将 Alice 带到了 social.mallory.com,她被欺骗提交了她的身份验证凭据和其他私人信息给 Mallory。

这种攻击是 持久型 XSS 的一个例子。一个易受攻击的系统通过持久化攻击者的恶意负载来启用这种形式的 XSS。后来,在受害者的浏览器中,通过受害者的错误,负载被注入。图 14.1 描述了这种攻击。


图 14.1 Mallory 的持久型 XSS 攻击将 Alice 引导至一个恶意冒牌站点。

设计用于共享用户内容的系统特别容易受到这种 XSS 的影响。此类系统包括社交媒体网站、论坛、博客和协作产品。像 Mallory 这样的攻击者通常比这更加激进。例如,这一次 Mallory 等待 Alice 无意中陷入陷阱。在现实世界中,攻击者通常会通过电子邮件或聊天主动引诱受害者访问注入的内容。

在本节中,Mallory 通过 Bob 的网站攻击了 Alice。在下一节中,Mallory 将通过 Alice 的一个网站攻击 Bob。

14.1.2 反射型 XSS

假设 Bob 是 Alice 新网站 search.alice.com 的用户。与 google.com 一样,该网站通过 URL 查询参数接受 Bob 的搜索词。作为回报,Bob 收到一个包含搜索结果的 HTML 页面。正如你所预料的那样,Bob 的搜索词被结果页面反映出来。

与其他搜索网站不同,search.alice.com 的结果页面会渲染用户的搜索词而不进行转义。Mallory 注意到了这一点,并准备了以下 URL。此 URL 的查询参数携带了恶意 JavaScript,通过 URL 编码进行了混淆。这个脚本旨在将 Bob 从 search.alice.com 带到另一个冒名顶替的网站 search.mallory.com:

https:/./search.alice.com/?terms=
➥ %3Cscript%3E                                          # ❶
➥ document.location=%27https://search.mallory.com%27    # ❶
➥ %3C/script%3E                                         # ❶

❶ 嵌入 URL 的脚本

Mallory 将这个 URL 发送给 Bob 的短信。他上钩了,点击了链接,无意中将 Mallory 的恶意代码发送给了 search.alice.com。网站立即将 Mallory 的恶意代码反射给了 Bob。Bob 的浏览器在渲染结果页面时执行了恶意脚本。最后,他被带到了 search.mallory.com,Mallory 进一步利用了他。

此攻击是反射型 XSS 的一个例子。攻击者通过诱使受害者向易受攻击的站点发送恶意有效载荷来发起这种形式的 XSS。该站点不会保留有效载荷,而是立即以可执行形式将有效载荷反射给用户。图 14.2 描绘了这种攻击。


图 14.2 Bob 将 Mallory 的恶意 JavaScript 从 Alice 的服务器反射出来,无意中将自己带到了 Mallory 的冒名顶替网站。

反射型 XSS 显然不仅限于聊天。攻击者还通过电子邮件或恶意网站引诱受害者。在下一节中,Mallory 用第三种 XSS 类型攻击 Charlie。与反射型 XSS 类似,这种类型的攻击也是从恶意 URL 开始的。

Python 全栈安全(三)(4)https://developer.aliyun.com/article/1508749

相关文章
|
2月前
|
存储 安全 数据安全/隐私保护
解锁Python安全新姿势!AES加密:让你的数据穿上防弹衣,无惧黑客窥探?
【8月更文挑战第1天】在数字化时代,确保数据安全至关重要。AES(高级加密标准)作为一种强大的对称密钥加密算法,能有效保护数据免遭非法获取。AES支持128/192/256位密钥,通过多轮复杂的加密过程提高安全性。在Python中,利用`pycryptodome`库可轻松实现AES加密:生成密钥、定义IV,使用CBC模式进行加密与解密。需要注意的是,要妥善管理密钥并确保每次加密使用不同的IV。掌握AES加密技术,为数据安全提供坚实保障。
150 2
|
1月前
|
存储 安全 数据安全/隐私保护
安全升级!Python AES加密实战,为你的代码加上一层神秘保护罩
【9月更文挑战第12天】在软件开发中,数据安全至关重要。本文将深入探讨如何使用Python中的AES加密技术保护代码免受非法访问和篡改。AES(高级加密标准)因其高效性和灵活性,已成为全球最广泛使用的对称加密算法之一。通过实战演练,我们将展示如何利用pycryptodome库实现AES加密,包括生成密钥、初始化向量(IV)、加密和解密文本数据等步骤。此外,还将介绍密钥管理和IV随机性等安全注意事项。通过本文的学习,你将掌握使用AES加密保护敏感数据的方法,为代码增添坚实的安全屏障。
102 8
|
1月前
|
存储 安全 算法
RSA在手,安全我有!Python加密解密技术,让你的数据密码坚不可摧
【9月更文挑战第11天】在数字化时代,信息安全至关重要。传统的加密方法已难以应对日益复杂的网络攻击。RSA加密算法凭借其强大的安全性和广泛的应用场景,成为保护敏感数据的首选。本文介绍RSA的基本原理及在Python中的实现方法,并探讨其优势与挑战。通过使用PyCryptodome库,我们展示了RSA加密解密的完整流程,帮助读者理解如何利用RSA为数据提供安全保障。
70 5
|
1月前
|
存储 安全 算法
显微镜下的安全战!Python加密解密技术,透视数字世界的每一个安全细节
【9月更文挑战第7天】在数字世界中,数据安全至关重要。Python加密解密技术如同显微镜下的精密工具,确保信息的私密性和完整性。以大型医疗机构为例,通过AES和RSA算法的结合,既能高效加密大量医疗数据,又能安全传输密钥,防止数据泄露。以下是使用Python的`pycryptodome`库实现AES加密和RSA密钥交换的简化示例。此方案不仅提高了数据安全性,还为数字世界的每个细节提供了坚实保障,引领我们迈向更安全的未来。
33 1
|
2月前
|
存储 安全 算法
显微镜下的安全战!Python加密解密技术,透视数字世界的每一个安全细节
【8月更文挑战第3天】在数字世界中,数据安全至关重要。以一家处理大量敏感医疗信息的医疗机构为例,采用Python实现的AES和RSA加密技术成为了守护数据安全的强大工具。AES因其高效性和安全性被用于加密大量数据,而RSA则保证了AES密钥的安全传输。通过使用Python的`pycryptodome`库,可以轻松实现这一加密流程。此案例不仅展示了如何有效保护敏感信息,还强调了在数据加密和密钥管理过程中需要注意的关键点,为构建更安全的数字环境提供了参考。
41 10
|
2月前
|
JSON 安全 数据安全/隐私保护
Python安全新篇章:OAuth与JWT携手,开启认证与授权的新时代
【8月更文挑战第6天】随着互联网应用的发展,安全认证与授权变得至关重要。本文介绍OAuth与JWT两种关键技术,并展示如何结合它们构建安全系统。OAuth允许用户授权第三方应用访问特定信息,无需分享登录凭证。JWT是一种自包含的信息传输格式,用于安全地传递信息。通过OAuth认证用户并获取JWT,可以验证用户身份并保护数据安全,为用户提供可靠的身份验证体验。
40 6
|
2月前
|
存储 安全 Python
[python]使用标准库logging实现多进程安全的日志模块
[python]使用标准库logging实现多进程安全的日志模块
|
2月前
|
JSON 数据库 开发者
FastAPI入门指南:Python开发者必看——从零基础到精通,掌握FastAPI的全栈式Web开发流程,解锁高效编码的秘密!
【8月更文挑战第31天】在当今的Web开发领域,FastAPI迅速成为开发者的热门选择。本指南带领Python开发者快速入门FastAPI,涵盖环境搭建、基础代码、路径参数、请求体处理、数据库操作及异常处理等内容,帮助你轻松掌握这一高效Web框架。通过实践操作,你将学会构建高性能的Web应用,并为后续复杂项目打下坚实基础。
77 0
|
2月前
|
存储 安全 数据安全/隐私保护
安全升级!Python AES加密实战,为你的代码加上一层神秘保护罩
【8月更文挑战第2天】数据安全至关重要,AES加密作为对称加密的标准之一,因其高效性与灵活性被广泛采用。本文通过实战演示Python中AES的应用,使用pycryptodome库进行安装及加密操作。示例代码展示了生成随机密钥与初始化向量(IV)、对数据进行加密及解密的过程。注意事项包括密钥管理和IV的随机性,以及加密模式的选择。掌握AES加密能有效保护敏感数据,确保信息安全无虞。
70 6
|
3月前
|
SQL 安全 数据库
构建安全的Python Web应用是一项持续的努力,需要开发者时刻保持警惕,并采用最佳实践来预防各种安全威胁
【7月更文挑战第26天】构建安全的Python Web应用是一项持续的努力,需要开发者时刻保持警惕,并采用最佳实践来预防各种安全威胁
62 9