如何使用内容安全策略保护 Django 应用

作者选择了编程少女作为Write for DOnations计划的捐赠对象。

介绍

当您访问网站时,会使用各种资源来加载和渲染它。例如,当您访问https://www.digitalocean.com时,您的浏览器直接从digitalocean.com下载HTML和CSS。但是,图像和其他资源是从assets.digitalocean.com下载的,并且分析脚本是从各自的域加载的。

一些网站使用大量不同的服务、样式和脚本来加载和渲染它们的内容,您的浏览器将执行所有这些内容。浏览器不知道代码是否恶意,因此开发人员有责任保护用户。由于网站上可能有许多资源,因此在浏览器中具有仅允许批准资源的功能是确保用户不会受到损害的好方法。这就是内容安全策略(CSPs)的用途。

使用CSP头,开发人员可以明确允许某些资源运行,同时阻止所有其他资源。由于大多数站点可能有上百个资源,每个资源必须针对其特定类别进行批准,因此实施CSP可能是一项繁琐的任务。但是,具有CSP的网站将更安全,因为它确保只允许批准的资源运行。

在这个教程中,您将在一个基本的Django应用程序中实现CSP。您将自定义CSP以允许某些域和内联资源运行。可选地,您也可以使用Sentry来记录违规行为。

先决条件

要完成本教程,您需要:

步骤1 —— 创建演示视图

在这一步中,您将修改应用程序处理视图的方式,以便您可以添加CSP支持。

作为先决条件,您已安装了Django并设置了一个示例项目。Django中的默认视图过于简单,无法展示CSP中间件的所有功能,因此您将为本教程创建一个简单的HTML页面。

转到您在先决条件中创建的项目文件夹:

  1. cd django-apps

django-apps目录中,创建您的虚拟环境。我们将其称为通用的env,但您应该使用一个对您和您的项目有意义的名称。

  1. virtualenv env

现在,使用以下命令激活虚拟环境:

  1. . env/bin/activate

在虚拟环境中,使用nano或您喜欢的文本编辑器,在项目文件夹中创建一个views.py文件:

  1. nano django-apps/testsite/testsite/views.py

现在,您将添加一个基本视图,该视图将呈现您接下来将创建的index.html模板。将以下内容添加到views.py中:

django-apps/testsite/testsite/views.py
from django.shortcuts import render

def index(request):
    return render(request, "index.html")

完成后保存并关闭文件。

在一个新的templates目录中创建一个index.html模板:

mkdir django-apps/testsite/testsite/templates
nano django-apps/testsite/testsite/templates/index.html

将以下内容添加到index.html中:

django-apps/testsite/testsite/templates/index.html
<!DOCTYPE html>
<html>
    <head>
        <title>Hello world!</title>
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
        <link
            href="https://fonts.googleapis.com/css2?family=Yellowtail&display=swap"
            rel="stylesheet"
        />
        <style>
            h1 {
                font-family: "Yellowtail", cursive;
                margin: 0.5em 0 0 0;
                color: #0069ff;
                font-size: 4em;
                line-height: 0.6;
            }

            img {
                border-radius: 100%;
                border: 6px solid #0069ff;
            }

            .center {
                text-align: center;
                position: absolute;
                top: 50vh;
                left: 50vw;
                transform: translate(-50%, -50%);
            }
        </style>
    </head>
    <body>
        <div class="center">
            <img src="https://html.sammy-codes.com/images/small-profile.jpeg" />
            <h1>Hello, Sammy!</h1>
        </div>
    </body>
</html>

我们创建的视图将呈现此简单的HTML页面。它将显示文本Hello, Sammy!以及Sammy the Shark的图像。

完成后保存并关闭文件。

要访问此视图,您需要更新urls.py

  1. nano django-apps/testsite/testsite/urls.py

导入views.py文件,并通过添加高亮显示的行添加一个新路由:

django-apps/testsite/testsite/urls.py
from django.contrib import admin
from django.urls import path
from . import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('', views.index),
]

您刚创建的新视图现在可以在访问/时查看(应用程序正在运行时)。

完成后保存并关闭文件。

最后,您需要更新settings.py中的INSTALLED_APPS以包括testsite

  1. nano django-apps/testsite/testsite/settings.py
django-apps/testsite/testsite/settings.py
# ...
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'testsite',
]
# ...

settings.py中将testsite添加到应用程序列表中,以便Django可以对项目的结构做出一些假设。在这种情况下,它将假定templates文件夹包含用于渲染视图的Django模板。

从项目的根目录(testsite)开始,使用以下命令启动Django开发服务器,将your-server-ip替换为您自己服务器的IP地址。

  1. cd ~/django-apps/testsite
  2. python manage.py runserver your-server-ip:8000

打开浏览器访问your-server-ip:8000。页面应该看起来类似于这样:

此时,页面显示了Sammy the Shark的个人资料图片。在图像下方是蓝色脚本中的文本Hello, Sammy!

要停止Django开发服务器,请按CONTROL-C

在此步骤中,您创建了一个基本视图,作为Django项目的主页。接下来,您将为您的应用程序添加CSP支持。

步骤2 — 安装CSP中间件

在这一步中,您将安装并实现一个CSP中间件,以便您可以添加CSP头并在您的视图中使用CSP功能。中间件为Django处理的任何请求或响应添加了额外的功能。在这种情况下,Django-CSP中间件为Django响应添加了CSP支持。

首先,您将使用pip在您的Django项目中安装Mozilla的CSP中间件,即Python的包管理器。使用以下命令从PyPi(Python包索引)安装所需的软件包。要运行命令,您可以通过使用CONTROL-C停止Django开发服务器,或者在终端中打开一个新的选项卡:

  1. pip install django-csp

接下来,将中间件添加到您的Django项目的设置中。打开settings.py

  1. nano testsite/testsite/settings.py

安装了django-csp之后,您现在可以在settings.py中添加中间件。这将向您的响应添加CSP头部。
将以下行添加到MIDDLEWARE配置数组中:

testsite/testsite/settings.py
MIDDLEWARE = [
    'csp.middleware.CSPMiddleware',
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

完成后保存并关闭文件。您的Django项目现在支持CSP。在下一步中,您将开始添加CSP头部。

第三步 — 实施CSP头部

现在您的项目支持CSP,可以进行安全硬化。为了实现这一目标,您将配置项目以向响应添加CSP头部。CSP头部是告诉浏览器在遇到特定类型内容时应该如何行为的东西。因此,如果头部指示仅允许来自特定域的图像,则浏览器将仅允许来自该域的图像。

使用nano或您喜欢的文本编辑器打开settings.py

  1. nano testsite/testsite/settings.py

在文件中的任何位置定义以下变量:

testsite/testsite/settings.py
# 内容安全策略

CSP_IMG_SRC = ("'self'")

CSP_STYLE_SRC = ("'self'")

CSP_SCRIPT_SRC = ("'self'")

这些规则是CSP的模板。这些行指示允许图像、样式表和脚本的来源。目前,它们都包含字符串'self',这意味着只允许来自您自己域的资源。

完成后保存并关闭文件。

使用以下命令运行您的Django项目:

  1. python manage.py runserver your-server-ip:8000

当您访问your-server-ip:8000时,您会发现网站已损坏:

正如预期的那样,图像不会显示,文本以默认样式(粗体黑色)显示。这意味着CSP头正在执行,并且我们的页面现在更加安全。因为您之前创建的视图正在引用不属于您自己域的样式表和图像,所以浏览器将阻止它们。

您的项目现在具有一个可工作的CSP,告诉浏览器阻止不属于您域的资源。接下来,您将修改CSP以允许特定资源,这将修复首页缺失的图像和样式。

第4步 — 修改CSP以允许外部资源

现在您有了基本的CSP,您将根据您网站上使用的内容对其进行修改。例如,一个使用Adobe字体和嵌入式YouTube视频的网站将需要允许这些资源。然而,如果您的网站只显示来自自己域内的图像,您可以将图像设置保持为其限制性默认值。

第一步是找到您需要批准的每个资源。您可以使用浏览器的开发者工具来完成这项任务。在检查元素中打开网络监视器,刷新页面,然后查看被阻止的资源:

网络日志显示CSP正在阻止两个资源:来自fonts.googleapis.com的样式表和来自html.sammy-codes.com的图像。要在CSP头部允许这些资源,您需要修改settings.py中的变量。

要允许来自外部域的资源,请将域添加到与文件类型匹配的CSP部分。因此,要允许来自html.sammy-codes.com的图像,您将把html.sammy-codes.com添加到CSP_STYLE_SRC中。

打开settings.py并将以下内容添加到CSP_STYLE_SRC变量中:

testsite/testsite/settings.py
CSP_IMG_SRC = ("'self'", 'https://html.sammy-codes.com')

现在,网站不仅允许来自您域的图像,还允许来自html.sammy-codes.com的图像。

索引视图使用 Google Fonts。Google 会向您的站点提供字体(来自https://fonts.gstatic.com)以及应用它们的样式表(来自https://fonts.googleapis.com)。为了允许字体加载,请将以下内容添加到您的 CSP:

testsite/testsite/settings.py
CSP_STYLE_SRC = ("'self'", 'https://fonts.googleapis.com')

CSP_FONT_SRC = ("'self'", 'https://fonts.gstatic.com/')

与允许来自html.sammy-codes.com的图片类似,您还将允许来自fonts.googleapis.com的样式表和来自fonts.gstatic.com的字体。为了提供背景,从fonts.googleapis.com加载的样式表用于应用字体。字体本身则从fonts.gstatic.com加载。

保存并关闭文件。

警告:类似于self,还有其他关键字,如unsafe-inlineunsafe-evalunsafe-hashes,可以在 CSP 中使用。强烈建议您避免在 CSP 中使用这些规则。尽管这些规则会使实施变得更容易,但它们可能被用来规避 CSP 并使其失效。

有关更多信息,请参阅“不安全的内联脚本”的 Mozilla 产品文档

现在,Google Fonts 将被允许在您的站点上加载样式和字体,并且html.sammy-codes.com将被允许加载图像。然而,当您访问服务器上的页面时,您可能会注意到现在只有图像正在加载。那是因为不允许在 HTML 中使用的内联样式来应用字体。您将在下一步中修复这个问题。

第5步 — 处理内联脚本和样式

此时,您已经修改了CSP以允许外部资源。但是,视图中的内联资源,例如样式和脚本仍然不被允许。在这一步中,您将使它们可以正常工作,以便应用字体样式。

允许内联脚本和样式有两种方法:使用nonce和哈希。如果您经常修改内联脚本和样式,请使用nonce以避免频繁更改CSP。如果您很少更新内联脚本和样式,则使用哈希是一个合理的方法。

使用nonce允许内联脚本

首先,您将使用nonce方法。Nonce是一个随机生成的令牌,对于每个请求都是唯一的。如果两个人访问您的网站,他们将分别获得嵌入在您批准的内联脚本和样式中的唯一的nonce。将nonce视为一次性密码,用于批准站点的某些部分在单个会话中运行。

要向您的项目添加nonce支持,您将在settings.py中更新CSP。打开该文件进行编辑:

  1. nano testsite/testsite/settings.py

settings.py文件中的CSP_INCLUDE_NONCE_IN中添加script-src

在文件中的任何位置定义CSP_INCLUDE_NONCE_IN并将'script-src'添加到其中:

testsite/testsite/settings.py
# 内容安全策略

CSP_INCLUDE_NONCE_IN = ['script-src']

CSP_INCLUDE_NONCE_IN指示您可以向其添加nonce属性的内联脚本。由于多个数据源支持非ces(例如,style-src),因此CSP_INCLUDE_NONCE_IN被处理为数组。

保存并关闭文件。

现在,当您在视图模板中添加nonce属性时,允许为内联脚本生成ces。要尝试这样做,您将使用一个简单的JavaScript片段。

打开index.html以进行编辑:

  1. nano testsite/testsite/templates/index.html

在HTML的<head>中添加以下片段:

testsite/testsite/templates/index.html
<script>
    console.log("Hello from the console!");
</script>

此片段将Hello from the console!"打印到浏览器的控制台。但是,由于您的项目具有仅在内联脚本具有nonce时允许其运行的CSP,因此此脚本将无法运行,并将产生错误。

当您刷新页面时,您可以在浏览器的控制台中看到此错误:

图像加载成功,因为您在上一步中允许了外部资源。如预期的那样,样式当前为默认值,因为您尚未允许内联样式。同样如预期的那样,控制台消息未打印并返回错误。您需要为其提供ces以批准它。

您可以通过将nonce="{{request.csp_nonce}}"添加为此脚本的属性来实现。打开index.html进行编辑,并按照以下示例添加突出显示的部分:

testsite/testsite/templates/index.html
<script nonce="{{request.csp_nonce}}">
    console.log("Hello from the console!");
</script>

完成后保存并关闭您的文件。

如果您刷新页面,脚本现在将会执行:

当您查看检查元素时,您会注意到该属性没有值:

该值出于安全原因而不显示。浏览器已经处理了该值。它被隐藏起来,以便任何具有DOM访问权限的脚本无法访问它并将其应用于其他脚本。如果您选择查看页面源代码,这是浏览器接收到的内容:

请注意,每次刷新页面时,nonce值都会更改。这是因为我们项目中的CSP中间件为每个请求生成了新的nonce

当浏览器接收到响应时,这些nonce值会附加到CSP头中:

浏览器对您的站点发出的每个请求都会为该脚本生成唯一的nonce值。由于nonce在CSP头中提供,这意味着Django服务器批准了该特定脚本的运行。

您已经更新了项目以使用nonce,它可以应用于多个资源。例如,您也可以将其应用于样式表,通过将CSP_INCLUDE_NONCE_IN更新为允许style-src。但是,批准内联资源的更简单方法是您接下来要做的事情。

使用哈希允许内联样式

允许内联脚本和样式的另一种方法是使用哈希。哈希是给定内联资源的唯一标识符。

例如,这是我们模板中的内联样式:

testsite/testsite/templates/index.html
<style>
    h1 {
        font-family: "Yellowtail", cursive;
        margin: 0.5em 0 0 0;
        color: #0069ff;
        font-size: 4em;
        line-height: 0.6;
    }

    img {
        border-radius: 100%;
        border: 6px solid #0069ff;
    }

    .center {
        text-align: center;
        position: absolute;
        top: 50vh;
        left: 50vw;
        transform: translate(-50%, -50%);
    }
</style>

但目前样式无法正常工作。当您在浏览器中查看站点时,图像成功加载,但字体和样式未应用:

在浏览器的控制台中,您会发现内联样式违反了 CSP 的错误。(可能还有其他错误,但请查找关于内联样式的错误。)

产生错误是因为样式未经 CSP 批准。但是,请注意错误提供了批准样式片段所需的哈希。此哈希对于此特定样式片段是唯一的。不会有其他片段会具有相同的哈希。将此哈希放入 CSP 中,每当加载此特定样式时,它都将被批准。但是,如果您修改这些样式,您需要获取新的哈希并在 CSP 中用其替换旧的哈希。

现在,您将通过将其添加到 settings.py 中的 CSP_STYLE_SRC 来应用哈希,如下所示:

  1. nano testsite/testsite/settings.py
testsite/testsite/settings.py
CSP_STYLE_SRC = ("'self' 'sha256-r5bInLZB0y6ZxHFpmz7cjyYrndjwCeDLDu/1KeMikHA='", 'https://fonts.googleapis.com')

sha256-... 哈希添加到 CSP_STYLE_SRC 列表中将允许浏览器加载样式表而无需任何错误。

保存并关闭文件。

现在,在浏览器中重新加载网站,字体和样式应该成功加载:

内联样式和脚本现在正常运行。在这一步中,您使用了两种不同的方法,即一次性令牌和哈希,以允许内联样式和脚本。

但是,有一个重要的问题需要解决。CSP 很麻烦,特别是对于大型网站来说。您可能需要一种方式来跟踪当 CSP 阻止资源时,以便确定它是否是恶意资源还是您网站的一个简单的故障部分。在下一步中,您将使用 Sentry 记录并跟踪 CSP 产生的所有违规行为。

第6步 — 使用 Sentry 报告违规行为(可选)

考虑到 CSP 通常是严格的,知道它何时阻止内容是很好的 —— 特别是因为阻止内容很可能意味着您网站上的某些功能无法正常工作。像 Sentry 这样的工具可以让您知道 CSP 正在阻止用户的请求。在这一步中,您将配置 Sentry 记录和报告 CSP 违规行为。

作为先决条件,您已在 Sentry 注册了一个帐户。现在您将创建一个项目。

在 Sentry 仪表板的左上角,单击 项目 选项卡:

在右上角,单击 创建项目 按钮:

你会看到一些带有标题为选择平台的标志。选择Django:

然后,在底部,命名你的项目(例如,我们将使用sammys-tutorial),然后点击创建项目按钮:

Sentry会给你一个代码片段,添加到你的settings.py文件中。保存这个片段,以便稍后添加。

在你的终端中,安装Sentry SDK:

  1. pip install --upgrade sentry-sdk

像这样打开settings.py

  1. nano testsite/testsite/settings.py

将以下内容添加到文件末尾,并确保用仪表板中的值替换SENTRY_DSN

testsite/testsite/settings.py
import sentry_sdk
from sentry_sdk.integrations.django import DjangoIntegration

sentry_sdk.init(
    dsn="SENTRY_DSN",
    integrations=[DjangoIntegration()],

    # 将 traces_sample_rate 设置为 1.0 以捕获 100%
    # 的交易以进行性能监控。
    # 我们建议在生产中调整此值。
    traces_sample_rate=1.0,

    # 如果您希望将用户关联到错误(假设您正在使用
    # django.contrib.auth),则可以启用发送 PII 数据。
    send_default_pii=True
)

此代码由Sentry提供,以便它可以记录应用程序中发生的任何错误。这是Sentry的默认配置,并初始化Sentry以记录服务器上的问题。从技术上讲,你不需要在你的服务器上初始化Sentry来处理CSP违规,但在极少数情况下,如果出现一些渲染非ces或哈希的问题,这些错误将被记录到Sentry中。

保存并关闭文件。

接下来,返回项目仪表板,并点击齿轮图标进入设置

进入安全标头选项卡:

复制report-uri

将其添加到您的 CSP 中,如下所示:

testsite/testsite/settings.py
# 内容安全策略

CSP_REPORT_URI = "your-report-uri"

确保将your-report-uri替换为您从仪表板中复制的值。

保存并关闭您的文件。现在,当 CSP 执行引起违规时,Sentry 将其记录到此 URI。您可以通过从 CSP 中移除域或哈希,或者从您之前添加的脚本中移除nonce来尝试此操作。在浏览器中加载页面,您将在 Sentry 的问题页面中看到错误:

如果您发现日志过多,还可以在settings.py中定义CSP_REPORT_PERCENTAGE,仅将一部分日志发送到 Sentry。

testsite/testsite/settings.py
# 内容安全策略
# 将日志的 10% 发送到 Sentry
CSP_REPORT_PERCENTAGE = 0.1

现在,每当出现 CSP 违规时,您将收到通知,并可以在 Sentry 中查看错误。

结论

在本文中,您使用内容安全策略(CSP)保护了您的Django应用程序。您更新了策略以允许外部资源,并使用了一次性随机数和哈希来允许内联脚本和样式。您还配置了它以将违规行为发送到Sentry。作为下一步,请查看Django CSP文档以了解如何强制执行您的CSP。

Source:
https://www.digitalocean.com/community/tutorials/how-to-secure-your-django-application-with-a-content-security-policy