Python 全栈安全(三)(2)

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

Python 全栈安全(三)(1)https://developer.aliyun.com/article/1508744

11.4 requests-oauthlib

requests-oauthlib是在 Python 中实现 OAuth 客户端的出色库。此库将另外两个组件粘合在一起:requests包和oauthlib。在您的虚拟环境中,运行以下命令来安装requests_oauthlib

$ pipenv install requests_oauthlib

在第三方项目中声明一些常量,从客户端注册凭据开始。在本例中,我将客户端密钥存储在 Python 中。在生产系统中,您的客户端密钥应该安全地存储在密钥管理服务中,而不是您的代码库中:

CLIENT_ID = 'Q7kuJVjbGbZ6dGlwY49eFP7fNFEUFrhHGGG84aI3'
CLIENT_SECRET = 'YyP1y8BCCqfsafJr0Lv9RcOVeMjdw3HqpvIPJeRjXB...'

接下来,定义授权表单、令牌交换端点和受保护资源的 URL:

AUTH_SERVER = 'https:/./authorize.alice.com'
AUTH_FORM_URL = '%s/o/authorize/' % AUTH_SERVER
TOKEN_EXCHANGE_URL = '%s/o/token/' % AUTH_SERVER
RESOURCE_URL = 'https:/./resource.alice.com/protected/email/'

域名

在本章中,我使用诸如authorize.alice.comclient.charlie.com等域名,以避免将您与对 localhost 的含糊引用混淆。为了跟上内容,您不必在本地开发环境中这样做;使用 localhost 就可以了。

只需确保你的第三方服务器绑定到与授权服务器不同的端口即可。服务器的端口由bind参数指定,如下所示加粗显示:

$ gunicorn third.wsgi --bind localhost:8001 \              # ❶
                      --keyfile path/to/private_key.pem \
                      --certfile path/to/certificate.pem

❶ 将服务器绑定到 8001 端口

在下一节中,你将使用这些配置设置来请求授权、获取访问令牌和访问受保护资源。

11.4.1 OAuth 客户端职责

requests-oauthlib 使用 OAuth2Session 处理 OAuth 客户端的职责,它是 Python OAuth 客户端的瑞士军刀。该类旨在自动完成以下操作:

  • 生成授权 URL
  • 将授权码交换为访问令牌
  • 请求受保护资源
  • 撤销访问令牌

将列表 11.4 中的视图添加到你的第三方项目中。WelcomeView 在用户的 HTTP 会话中查找访问令牌。然后,它请求两者之一:用户的授权或来自资源服务器的电子邮件。如果没有访问令牌可用,则渲染一个带有授权 URL 的欢迎页面;如果有访问令牌可用,则渲染一个带有用户电子邮件的欢迎页面。

列表 11.4 OAuth 客户端 WelcomeView

from django.views import View
from django.shortcuts import render
from requests_oauthlib import OAuth2Session
class WelcomeView(View):
    def get(self, request):
        access_token = request.session.get('access_token')
        client = OAuth2Session(CLIENT_ID, token=access_token)
        ctx = {}
        if not access_token:
            url, state = client.authorization_url(AUTH_FORM_URL)    # ❶
            ctx['authorization_url'] = url                          # ❶
            request.session['state'] = state                        # ❶
        else:
            response = client.get(RESOURCE_URL)                     # ❷
            ctx['email'] = response.json()['email']                 # ❷
        return render(request, 'welcome.html', context=ctx)

❶ 请求授权

❷ 访问受保护资源

OAuth2Session 用于生成授权 URL 或检索受保护资源。请注意,状态值的副本存储在用户的 HTTP 会话中;期望授权服务器在协议的后续阶段回显此值。

接下来,将以下欢迎页面模板添加到你的第三方项目中。如果用户的电子邮件已知,则渲染用户的电子邮件。否则,渲染授权链接(加粗显示):

<html>
    <body>
        {% if email %}
            Email: {{ email }}
        {% else %}
            <a href='{{ authorization_url }}'>    <!-- ❶ -->
                What is your email?               <!-- ❶ -->
            </a>                                  <!-- ❶ -->
        {% endif %}
    </body>
</html>

❶ 请求授权

请求授权

有许多请求授权的方法。在本章中,我为了简单起见使用链接来完成。或者,你可以通过重定向来完成。此重定向可以在 JavaScript、视图或自定义中间件组件中进行。

接下来,将列表 11.5 中的视图添加到你的第三方项目中。与 WelcomeView 一样,OAuthCallbackView 首先通过会话状态初始化 OAuth2Session。此视图将令牌交换委托给 OAuth2Session,并提供重定向 URI 和客户端密钥。然后将访问令牌存储在用户的 HTTP 会话中,WelcomeView 可以访问它。最后,用户被重定向回欢迎页面。

列表 11.5 OAuth 客户端 OAuthCallbackView

from django.shortcuts import redirect
from django.urls import reverse
from django.views import View
class OAuthCallbackView(View):
    def get(self, request):
        state = request.session.pop('state')
        client = OAuth2Session(CLIENT_ID, state=state)
        redirect_URI = request.build_absolute_uri()
        access_token = client.fetch_token(          # ❶
            TOKEN_EXCHANGE_URL,                     # ❶
            client_secret=CLIENT_SECRET,            # ❶
            authorization_response=redirect_URI)    # ❶
        request.session['access_token'] = access_token
        return redirect(reverse('welcome'))         # ❷

❶ 请求授权

❷ 将用户重定向回欢迎页面

fetch_token 方法为 OAuthCallbackView 执行了大量工作。首先,此方法从重定向 URI 中解析代码和状态参数。然后,它将入站状态参数与从用户的 HTTP 会话中提取的状态进行比较。如果两个值不匹配,则引发 MismatchingStateError,并且授权码永远不会被使用。如果两个状态值匹配,则 fetch_token 方法将授权码和客户端密钥发送到令牌交换端点。

撤销令牌

当你完成一个访问令牌后,通常没有理由继续持有它。你不再需要它,而且只有当它落入错误的手中时才会对你造成危害。因此,通常最好在访问令牌完成其目的后撤销每个访问令牌。一旦被撤销,访问令牌就无法用于访问受保护的资源。

DOT 通过一个专门的端点来处理令牌撤销。这个端点需要一个访问令牌和 OAuth 客户端凭据。以下代码演示了如何访问令牌撤销。请注意,资源服务器会用 403 状态码回应后续请求:

>>> data = {
...     'client_id': CLIENT_ID,
...     'client_secret': CLIENT_SECRET,
...     'token': client.token['access_token']
... }
>>> client.post('%s/o/revoke_token/' % AUTH_SERVER, data=data)    # ❶
<Response [200]>                                                  # ❶
>>> client.get(RESOURCE_URL)                                      # ❷
<Response [403]>                                                  # ❷

❶ 撤销访问令牌

❷ 后续访问被拒绝

大型 OAuth 提供商通常允许你手动撤销为你的个人数据发布的访问令牌。例如,访问myaccount.google.com/permissions查看为你的 Google 账户发布的所有有效访问令牌的列表。这个用户界面让你查看每个访问令牌的详细信息,并撤销它们。为了保护你的隐私,你应该撤销对任何你不打算很快使用的客户端应用程序的访问权限。

在这一章中,你学到了很多关于 OAuth 的知识。你从资源所有者、OAuth 客户端、授权服务器和资源服务器的角度了解了这个协议是如何工作的。你还接触到了 Django OAuth Toolkit 和requests-oauthlib。这些工具在它们的工作中表现出色,文档完善,并且彼此之间相互配合良好。

总结

  • 你可以在不分享密码的情况下分享你的数据。
  • 授权码流是目前最常用的 OAuth 授权类型。
  • 授权码被交换为访问令牌。
  • 通过限制访问令牌的时间和范围来降低风险。
  • 范围由 OAuth 客户端请求,由授权服务器定义,并由资源服务器强制执行。

第三部分:攻击抵抗

与第 1 部分和第 2 部分不同,第 3 部分主要关注的不是基础知识或发展。相反,一切都围绕着 Mallory 展开,她用跨站脚本、开放式重定向攻击、SQL 注入、跨站请求伪造、点击劫持等攻击摧毁其他角色。这是书中最具对抗性的部分。在每一章中,攻击不是为了补充主要思想;攻击就是主要思想。

第十二章:使用操作系统

本章内容包括

  • 使用os模块强制执行文件系统级别的授权
  • 使用tempfile模块创建临时文件
  • 使用subprocess模块调用外部可执行文件
  • 抵御 shell 注入和命令注入

最近的几章都涉及授权。你学习了用户、组和权限。我通过将这些概念应用于文件系统访问来开始本章。此后,我将向你展示如何安全地从 Python 中调用外部可执行文件。在此过程中,你将学习如何识别和抵御两种类型的注入攻击。这为本书的其余部分奠定了基调,专注于攻击抵御。

12.1 文件系统级别的授权

像大多数编程语言一样,Python 本地支持文件系统访问;不需要第三方库。文件系统级别的授权比应用程序级别的授权工作量少,因为你不需要执行任何操作;你的操作系统已经做了这个。在这一部分中,我将向你展示如何执行以下操作:

  • 安全地打开文件
  • 安全地创建临时文件
  • 读取和修改文件权限

12.1.1 请求权限

在过去几十年里,Python 社区中出现了许多缩写词。其中一个代表一种编码风格,称为宁愿请求宽恕,而不是先请求允许EAFP)。EAFP 风格假设前提条件为真,然后在它们为假时捕获异常。

例如,以下代码假设具有足够的访问权限来打开文件。程序不尝试询问操作系统是否有权限读取文件;相反,如果权限被拒绝,程序通过except语句请求宽恕:

try:
    file = open(path_to_file)   # ❶
except PermissionError:         # ❷
    return None                 # ❷
else:
    with file:
        return file.read()

❶ 假设权限,不要求权限

❷ 请求宽恕

EAFP 与另一种编码风格相对应,称为先尝试,再请求允许LBYL)。这种风格首先检查前提条件,然后执行。EAFP 的特点是tryexcept语句;LBYL 的特点是ifthen语句。EAFP 被称为乐观;LBYL 被称为悲观

以下代码是 LBYL 的一个示例;它打开一个文件,但首先查看它是否具有足够的访问权限。注意,这段代码容易受到意外和恶意竞争条件的影响。一个错误或攻击者可能利用os.access函数返回和调用open函数之间的时间间隔。这种编码风格还会导致更多的文件系统访问:

if os.access(path_to_file, os.R_OK):    # ❶
    with open(path_to_file) as file:    # ❷
        return file.read()              # ❷
return None

❶ 看

❷ 跳

Python 社区中有些人强烈偏爱 EAFP 而不是 LBYL;我不是其中之一。我没有偏好,我根据具体情况使用两种风格。在这个特定的案例中,出于安全考虑,我使用 EAFP 而不是 LBYL。

EAFP 对比 LBYL

显然,Python 的创始人 Guido van Rossum 对 EAFP 也没有强烈偏好。Van Rossum 曾在 Python-Dev 邮件列表中写道(mail.python.org/pipermail/python-dev/2014-March/133118.html):

. . . 我不同意 EAFP 比 LBYL 更好,或者“Python 通常推荐”的立场。(你从哪里得到的?从那些如此痴迷于 DRY,宁愿引入高阶函数而不重复一行代码的来源? 😃

12.1.2 使用临时文件

Python 本身支持使用专用模块 tempfile 进行临时文件使用;在处理临时文件时无需生成子进程。tempfile 模块包含一些高级工具和一些低级函数。这些工具以最安全的方式创建临时文件。以这种方式创建的文件不可执行,只有创建用户可以读取或写入它们。

tempfile.TemporaryFile 函数是创建临时文件的首选方式。这个高级工具创建一个临时文件并返回其对象表示。当您在 with 语句中使用这个对象时,如下面代码中所示,它会为您关闭和删除临时文件。在这个例子中,创建一个临时文件,打开,写入,读取,关闭和删除:

>>> from tempfile import TemporaryFile
>>> 
>>> with TemporaryFile() as tmp:                           # ❶
...     tmp.write(b'Explicit is better than implicit.')    # ❷
...     tmp.seek(0)                                        # ❸
...     tmp.read()                                         # ❸
...                                                        # ❹
33
0
b'Explicit is better than implicit.'

❶ 创建并打开一个临时文件

❷ 写入文件

❸ 从文件中读取

❹ 退出块,关闭并删除文件

TemporaryFile 有一些替代方案来解决边缘情况。如果需要一个具有可见名称的临时文件,请将其替换为 NamedTemporaryFile。如果需要在将数据写入文件系统之前在内存中缓冲数据,请将其替换为 SpooledTemporaryFile

tempfile.mkstemptempfile.mkdtemp 函数是创建临时文件和临时目录的低级替代方案,分别。这些函数安全地创建临时文件或目录并返回路径。这与前述高级工具一样安全,但您必须承担关闭和删除使用它们创建的每个资源的责任。

警告 不要混淆 tempfile.mkstemptempfile.mkdtemptempfile.mktemp。这些函数的名称仅相差一个字符,但它们是非常不同的。tempfile.mktemp 函数由于安全原因已被 tempfile.mkstemptempfile.mkdtemp 废弃。

永远不要使用tempfile.mktemp。过去,这个函数被用来生成一个未使用的文件系统路径。调用者然后会使用这个路径来创建和打开一个临时文件。不幸的是,这是另一个你不应该使用 LBYL 编程的例子。考虑一下mktemp返回和临时文件创建之间的时间窗口。在这段时间内,攻击者可以在相同的路径上创建一个文件。从这个位置,攻击者可以向系统信任的文件写入恶意内容。

12.1.3 处理文件系统权限

每个操作系统都支持用户和组的概念。每个文件系统都维护关于每个文件和目录的元数据。用户、组和文件系统元数据决定操作系统如何执行文件系统级别的授权。在本节中,我将介绍几个设计用于修改文件系统元数据的 Python 函数。不幸的是,这些功能在只有类 UNIX 系统上完全支持。

类 UNIX 文件系统元数据指定一个所有者、一个组和三个类别:用户、组和其他人。每个类别代表三个权限:读取、写入和执行。用户和组类别适用于分配给文件的所有者和组。其他类别适用于其他所有人。

例如,假设 Alice、Bob 和 Mallory 有操作系统账户。一个由 Alice 拥有的文件分配给一个名为observers的组。Bob 是这个组的成员;Alice 和 Mallory 不是。这个文件的权限和类别由表 12.1 的行和列表示。

表 12.1 按类别的权限

拥有者 其他
读取
写入
执行

当 Alice、Bob 或 Mallory 尝试访问文件时,操作系统仅应用最本地类别的权限:

  • 作为文件的所有者,Alice 可以读取和写入文件,但不能执行它。
  • 作为observers的成员,Bob 可以读取文件,但不能对其进行写入或执行。
  • Mallory 根本无法访问文件,因为她既不是所有者也不在observers中。

Python 的os模块具有几个设计用于修改文件系统元数据的函数。这些函数允许 Python 程序直接与操作系统通信,消除了调用外部可执行文件的需要:

  • os.chmod—修改访问权限
  • os.chown—修改所有者 ID 和组 ID
  • os.stat—读取用户 ID 和组 ID

os.chmod函数修改文件系统权限。该函数接受一个路径和至少一个模式。每个模式在stat模块中被定义为一个常量,在表 12.2 中列出。在 Windows 系统上,os.chmod不幸地只能改变文件的只读标志。

表 12.2 权限模式常量

模式 拥有者 其他
读取 S_IRUSR S_IRGRP S_IROTH
写入 S_IWUSR S_IWGRP S_IWOTH
执行 S_IXUSR S_IXGRP S_IXOTH

以下代码演示了如何使用 os.chmod。第一次调用授予所有者读取权限;所有其他权限都被拒绝。此状态通过后续对 os.chmod 的调用而被擦除,而不是修改。这意味着第二次调用授予了群组读取权限;所有其他权限,包括先前授予的权限,都被拒绝:

import os
import stat
os.chmod(path_to_file, stat.S_IRUSR)    # ❶
os.chmod(path_to_file, stat.S_IRGRP)    # ❷

❶ 只有所有者可以阅读此内容。

❷ 只有群组可以阅读此内容。

如何授予多个权限?使用 OR 运算符组合模式。例如,以下代码行同时向所有者和群组授予读取访问权限:

os.chmod(path_to_file, stat.S_IRUSR | stat.S_IRGRP)    # ❶

❶ 只有所有者和群组可以阅读此内容。

os.chown 函数修改文件或目录的所有者和群组。此函数接受路径、用户 ID 和群组 ID。如果将 -1 作为用户 ID 或群组 ID 传递,则相应的 ID 将保持不变。下面的示例演示了如何在保留群组 ID 的同时更改您的 settings 模块的用户 ID。在您自己的系统上运行此代码是不明智的:

os.chown(path_to_file, 42, -1)

os.stat 函数返回文件或目录的元数据。此元数据包括用户 ID 和群组 ID。在 Windows 系统上,这些 ID 不幸地始终为 0。在交互式 Python shell 中键入以下代码以获取您的 settings 模块的用户 ID 和群组 ID,如加粗所示:

>>> import os
>>> 
>>> path = './alice/alice/settings.py'
>>> stat = os.stat(path)
>>> stat.st_uid             # ❶
501                         # ❶
>>> stat.st_gid             # ❷
20                          # ❷

❶ 访问用户 ID

❷ 访问群组 ID

在本节中,您学习了如何创建与文件系统交互的程序。在下一节中,您将学习如何创建运行其他程序的程序。

12.2 调用外部可执行文件

有时,您想要在 Python 中执行另一个程序。例如,您可能希望练习使用非 Python 语言编写的程序的功能。Python 提供了许多调用外部可执行文件的方法;其中一些方法可能存在风险。在本节中,我将为您提供一些工具来识别、避免和最小化这些风险。

警告:本节中许多命令和代码具有潜在破坏性。在为本章测试代码时,我曾意外地从笔记本电脑上删除了一个本地 Git 仓库。如果您选择运行以下任何示例,请自己小心。

当您在计算机上键入并执行命令时,您并没有直接与操作系统通信。相反,您键入的命令被另一个称为 shell 的程序传递到您的操作系统。例如,如果您在类 UNIX 系统上,您的 shell 可能是 /bin/bash。如果您在 Windows 系统上,您的 shell 可能是 cmd.exe。图 12.1 描述了 shell 的作用。(虽然图表显示的是 Linux 操作系统,但在 Windows 系统上的过程类似。)


图 12.1 一个 bash shell 将 Alice 的终端上的命令传递给操作系统。

如其名称所示,shell 仅提供了一层薄薄的功能。其中一些功能是由特殊字符支持的。特殊字符具有超出其字面用途的含义。例如,类 Unix 系统的 shell 将星号(*)字符解释为通配符。这意味着诸如rm *这样的命令会删除当前目录中的所有文件,而不是删除一个(奇怪地)命名为*的单个文件。这称为通配符展开

如果要求 shell 按字面意义解释特殊字符,则必须使用转义字符。例如,类 Unix 系统的 shell 将反斜杠视为转义字符。这意味着如果你只想删除一个(奇怪地)命名为*的文件,你必须输入rm \*

从外部来源构建命令字符串而不转义特殊字符可能是致命的。例如,以下代码演示了一种糟糕的调用外部可执行文件的方式。此代码提示用户输入文件名并构建命令字符串。然后,os.system函数执行该命令,删除文件,并返回 0。按照惯例,返回代码 0 表示命令成功完成。当用户键入alice.txt时,此代码表现正常,但是如果恶意用户键入*,则会删除当前目录中的所有文件。这称为shell 注入攻击

>>> import os
>>> 
>>> file_name = input('Select a file for deletion:')   # ❶
Select a file for deletion: alice.txt                  # ❶
>>> command = 'rm %s' % file_name
>>> os.system(command)                                 # ❷
0                                                      # ❷

❶ 从不受信任的来源接受输入

❷ 成功执行命令

除了 shell 注入之外,此代码还容易受到命令注入的攻击。例如,如果恶意用户提交-rf / ; dd if=/dev/random of=/dev/sda,则此代码将运行两个命令而不是一个。第一个命令删除根目录中的所有内容;第二个命令则通过向硬盘写入随机数据进一步恶化了情况。

Shell 注入和命令注入都是更广泛的攻击类别的特殊类型,通常称为注入攻击。攻击者通过向易受攻击的系统注入恶意输入来发起注入攻击。系统然后无意中执行输入,试图处理它,从而在某种程度上使攻击者受益。

注意:在撰写本文时,注入攻击位列 OWASP 十大安全威胁的第一位(owasp.org/www-project-top-ten/)。

在接下来的两节中,我将演示如何避免 shell 注入和命令注入。

12.2.1 使用内部 API 绕过 shell

如果你执行外部程序,你应该首先问自己是否需要。在 Python 中,答案通常是否定的。Python 已经为最常见的问题开发了内部解决方案;在这些情况下,没有必要调用外部可执行文件。例如,以下代码使用os.remove而不是os.system删除文件。这样的解决方案更容易编写,更容易阅读,更少出错,更安全:

>>> file_name = input('Select a file for deletion:')    # ❶
Select a file for deletion:bob.txt                      # ❶
>>> os.remove(file_name)                                # ❷

❶ 从不受信任的来源接受输入

❷ 删除文件

这种替代方案更安全在哪里?与 os.system 不同,os.remove 免疫于命令注入,因为它只做一件事,这是设计原则;这个函数不接受命令字符串,因此没有办法注入其他命令。此外,os.remove 避免了 shell 注入,因为它完全绕过了 shell;这个函数直接与操作系统交流,而不需要 shell 的帮助,也没有 shell 的风险。如粗体所示,特殊字符如 * 被直接解释:

>>> os.remove('*')                                             # ❶
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: '*'    # ❷

❶ 这看起来不好 . . .

❷ . . . 但是没有东西被删除。

还有许多其他类似 os.remove 的函数;表格 12.3 列出了其中一些。第一列表示一个不必要的命令,第二列表示纯 Python 的替代方案。这个表格中的一些解决方案应该看起来很熟悉;在讨论文件系统级授权时,你已经见过它们。

表格 12.3 Python 替代简单命令行工具

命令行示例 Python 等价物 描述
$ chmod 400 bob.txt os.chmod(‘bob.txt’, S_IRUSR) 修改文件权限
$ chown bob bob.txt os.chown(‘bob.txt’, uid, -1) 更改文件所有者
$ rm bob.txt os.remove(‘bob.txt’) 删除文件
> mkdir new_dir os.mkdir(‘new_dir’) 创建新目录
> dir os.listdir() 列出目录内容
> pwd os.getcwd() 当前工作目录
$ hostname import socket;socket.gethostname() 读取系统主机名

如果 Python 没有为某个命令提供安全的替代方案,那么很可能会有一个开源的 Python 库提供。表格 12.4 列出了一组命令及其 PyPI 包的替代方案。你在前几章学到了其中的两个,requestscryptography

表格 12.4 Python 替代复杂命令行工具

命令行示例 PyPI 等价物 描述
$ curl http:/./bob.com -o bob.txt requests 通用 HTTP 客户端
$ openssl genpkey -algorithm RSA cryptography 通用加密
$ ping python.org ping3 测试主机是否可达
$ nslookup python.org nslookup 执行 DNS 查询
$ ssh alice@python.org paramiko SSH 客户端
$ git commit -m ‘Chapter 12’ GitPython 与 Git 仓库一起工作

表格 12.3 和 12.4 绝不是详尽无遗的。Python 生态系统中还有许多其他替代方案可用于外部可执行文件。如果你正在寻找一个不在这些表格中的纯 Python 替代方案,请在开始编写代码之前在网上搜索一下。

偶尔你可能会面临一个没有纯 Python 替代方案的独特挑战。例如,你可能需要运行一个你的同事编写的自定义 Ruby 脚本来解决领域特定的问题。在这种情况下,你需要调用一个外部可执行文件。在下一节中,我将向你展示如何安全地执行这样的操作。

12.2.2 使用 subprocess 模块

subprocess 模块是 Python 对外部可执行程序的答案。该模块废弃了 Python 的许多内置函数用于命令执行,列在这里。你在前一节中看到了其中之一:

  • os.system
  • os.popen
  • os.spawn*(八个函数)

subprocess 模块以简化的 API 和设计用于改善进程间通信、错误处理、互操作性、并发性和安全性的特性集取代了这些函数。在本节中,我只强调了该模块的安全特性。

以下代码使用 subprocess 模块从 Python 中调用一个简单的 Ruby 脚本。Ruby 脚本接受原型角色的名称,如 Alice 或 Eve;该脚本的输出是角色拥有的域的列表。请注意,run 函数不接受命令字符串;相反,它期望命令以列表形式提供,如粗体字所示。run 函数在执行后返回一个 CompletedProcess 实例。此对象提供对外部进程的输出和返回代码的访问:

>>> from subprocess import run
>>> 
>>> character_name = input('alice, bob, or charlie?')        # ❶
alice, bob, or charlie?charlie                               # ❶
>>> command = ['ruby', 'list_domains.rb', character_name]    # ❶
>>>
>>> completed_process = run(command, capture_output=True, check=True)
>>>
>>> completed_process.stdout                                 # ❷
b'charlie.com\nclient.charlie.com\n'                         # ❷
>>> completed_process.returncode                             # ❸
0                                                            # ❸

❶ 构建一个命令

❷ 打印命令输出

❸ 打印命令返回值

subprocess 模块从设计上是安全的。该 API 通过强制你将命令表达为列表来抵御命令注入。例如,如果一个恶意用户提交 charlie ; rm -fr / 作为一个角色名,run 函数仍然只执行 一个 命令,并且它执行的命令仍然只有 一个 (奇怪的)参数。

subprocess 模块 API 也抵御了 shell 注入。默认情况下,run 函数绕过 shell 并将命令直接转发给操作系统。在极为罕见的情况下,当你确实需要特殊功能(例如通配符展开)时,run 函数支持一个名为 shell 的关键字参数。顾名思义,将此关键字参数设置为 True 会通知 run 函数将你的命令传递给 shell。

换句话说,run 函数默认是安全的,但你可以明确选择一个更危险的选项。相反,os.system 函数默认是危险的,你别无选择。图 12.2 说明了两个函数及其行为。


图 12.2 Alice 运行了两个 Python 程序;第一个通过 shell 与操作系统通信,第二个直接与操作系统通信。

在本章中,你学到了两种类型的注入攻击。当你阅读下一章时,你会看到为什么这些攻击在 OWASP 十大中排名第一。它们有很多不同的形式和大小。

总结

  • 优先选择高级授权工具而不是低级方法。
  • 根据具体情况选择 EAFP 和 LBYL 编码风格。
  • 想要调用外部可执行程序与需要调用外部可执行程序是不同的。
  • 在 Python 和 PyPI 之间,通常有你想要的命令的替代方案。
  • 如果你需要执行一个命令,那么这个命令极有可能不需要一个 shell。

第十三章:永远不要信任输入

本章包括

  • 使用 Pipenv 验证 Python 依赖项
  • 使用 PyYAML 安全解析 YAML
  • 使用 defusedxml 安全解析 XML
  • 防止 DoS 攻击,Host 头攻击,开放重定向和 SQL 注入

在这一章中,Mallory 对 Alice、Bob 和 Charlie 发动了半打攻击。这些攻击及其对策并不像我后面涵盖的攻击那样复杂。本章中的每个攻击都遵循一种模式:Mallory 利用恶意输入滥用系统或用户。这些攻击以许多不同形式的输入形式出现:包依赖项、YAML、XML、HTTP 和 SQL。这些攻击的目标包括数据损坏、特权提升和未经授权的数据访问。输入验证是这些攻击的解药。

我在本章中涵盖的许多攻击都是注入攻击。(您在上一章中学习了关于注入攻击的知识。)在典型的注入攻击中,恶意输入被注入并立即由正在运行的系统执行。因此,程序员往往忽略了我在本章中开始讨论的非典型场景。在这种情况下,注入发生在上游,即构建时;执行发生在下游,即运行时。

13.1 使用 Pipenv 进行包管理

在本节中,我将向您展示如何使用 Pipenv 防止注入攻击。像之前学过的哈希和数据完整性一样,它们将再次出现。与任何 Python 包管理器一样,Pipenv 从诸如 PyPI 之类的包仓库检索并安装第三方包。不幸的是,程序员未能意识到包仓库是他们攻击面的重要部分。

假设 Alice 想要定期将新版本的 alice.com 部署到生产环境。她编写了一个脚本来拉取她代码的最新版本,以及她的软件包依赖项的最新版本。Alice 没有通过将她的依赖项检入版本控制来增加她代码仓库的大小。相反,她使用包管理器从包仓库拉取这些工件。

Mallory 已经入侵了 Alice 依赖的包仓库。在这个位置,Mallory 使用恶意代码修改了 Alice 的一个依赖项。最后,恶意代码由 Alice 的包管理器拉取并推送到 alice.com,在那里执行。图 13.1 说明了 Mallory 的攻击。


图 13.1 Mallory 通过包依赖注入恶意代码到 alice.com。

与其他包管理器不同,Pipenv 通过在从包仓库拉取每个包时验证包的完整性来自动阻止 Mallory 执行此攻击。如预期的那样,Pipenv 通过比较哈希值来验证包的完整性。

当 Pipenv 第一次获取一个包时,它会记录每个包构件的哈希值在你的锁定文件 Pipfile.lock 中。打开你的锁定文件,花一分钟观察一下你的一些依赖项的哈希值。例如,我的锁定文件的以下部分表明 Pipenv 拉取了requests包的 2.24 版本。两个构件的 SHA-256 哈希值以粗体显示:

...
"requests": {
 "hashes": [
 "Sha256:b3559a131db72c33ee969480840fff4bb6dd1117c8...", # ❶
 "Sha256:fe75cc94a9443b9246fc7049224f756046acb93f87..." # ❶
 ],
    "version": "==2.24.0"                                          # ❷
},
...

❶ 包构件的哈希值

❷ 包版本

当 Pipenv 获取一个熟悉的包时,它会对每个入站包构件进行哈希,并将哈希值与您的锁定文件中的哈希值进行比较。如果哈希值匹配,Pipenv 可以假定该包未经修改,因此安全安装。如果哈希值不匹配,如图 13.2 所示,Pipenv 将拒绝该包。


图 13.2 包管理器通过将恶意修改的 Python 包的哈希值与锁定文件中的哈希值进行比较来抵御注入攻击。

下面的命令输出展示了当一个包验证失败时 Pipenv 的行为。本地哈希值和警告以粗体显示:

$ pipenv install
Installing dependencies from Pipfile.lock
An error occurred while installing requests==2.24.0 
➥ --hash=sha256:b3559a131db72c33ee969480840fff4bb6dd1117c8...   # ❶
➥ --hash=sha256:fe75cc94a9443b9246fc7049224f756046acb93f87...   # ❶
...
[pipenv.exceptions.InstallError]: ['ERROR: THESE PACKAGES DO NOT
➥ MATCH THE HASHES FROM THE REQUIREMENTS FILE. If you have updated
➥ the package versions, please update the hashes. Otherwise,
➥ examine the package contents carefully; someone may have      # ❷
➥ tampered with them.                                           # ❷
...

❶ 包构件的本地哈希值

❷ 数据完整性警告

除了保护您免受恶意包修改之外,此检查还检测意外包损坏。这确保了本地开发、测试和生产部署的确定性构建——这是使用哈希进行现实世界数据完整性验证的一个很好的例子。在接下来的两节中,我将继续介绍注入攻击。

Python 全栈安全(三)(3)https://developer.aliyun.com/article/1508748

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