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

作者选择了 Girls Who Code以作为 Write for Donations计划的一部分获得捐款。

介绍

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

一些网站使用多种不同的服务,风格和脚本来加载和渲染其内容,您的浏览器将执行所有这些内容。浏览器不知道代码是否有害,因此开发人员有责任保护用户。

使用 CSP 标题,开发人员可以明确允许某些资源运行,同时阻止所有其他资源。因为大多数网站可以有超过 100 个资源,并且每个资源都必须被批准为其特定的资源类别,实施 CSP 可能是一项艰难的任务。

在本教程中,您将在基本的 Django 应用程序中实现 CSP. 您将定制 CSP 以允许某些域和内线资源运行。

前提条件

要完成本教程,您将需要:

如果你没有一个,你可以创建一个工作Django项目(版本3或更高),无论是在你的本地机器或DigitalOcean Droplet. 如果你没有一个,你可以创建一个教程, 如何在Ubuntu 20.04上安装Django并设置开发环境

步骤 1 - 创建一个演示视图

在此步骤中,您将更改应用程序如何处理视图,以便您可以添加 CSP 支持。

作为先决条件,你安装了Django并设置了一个样本项目.Django的默认视图太简单,无法展示CSP中间软件的所有功能,所以你将为本教程创建一个简单的HTML页面。

导航到您在前提条件中创建的项目文件夹:

1cd django-apps

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

1virtualenv env

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

1. env/bin/activate

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

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

现在,您将添加一个基本视图,该视图将显示您下次创建的index.html模板。

1[label django-apps/testsite/testsite/views.py]
2from django.shortcuts import render
3
4def index(request):
5    return render(request, "index.html")

保存并关闭文件,当你完成。

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

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

请在 index.html 中添加以下内容:

 1[label django-apps/testsite/testsite/templates/index.html]
 2<!DOCTYPE html>
 3<html>
 4    <head>
 5        <title>Hello world!</title>
 6        <link rel="preconnect" href="https://fonts.googleapis.com" />
 7        <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
 8        <link
 9            href="https://fonts.googleapis.com/css2?family=Yellowtail&display=swap"
10            rel="stylesheet"
11        />
12        <style>
13            h1 {
14                font-family: "Yellowtail", cursive;
15                margin: 0.5em 0 0 0;
16                color: #0069ff;
17                font-size: 4em;
18                line-height: 0.6;
19            }
20
21            img {
22                border-radius: 100%;
23                border: 6px solid #0069ff;
24            }
25
26            .center {
27                text-align: center;
28                position: absolute;
29                top: 50vh;
30                left: 50vw;
31                transform: translate(-50%, -50%);
32            }
33        </style>
34    </head>
35    <body>
36        <div class="center">
37            <img src="https://html.sammy-codes.com/images/small-profile.jpeg" />
38            <h1>Hello, Sammy!</h1>
39        </div>
40    </body>
41</html>

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

保存并关闭文件,当你完成。

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

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

导入views.py文件并通过添加突出的行添加新路线:

1[label django-apps/testsite/testsite/urls.py]
2from django.contrib import admin
3from django.urls import path
4from . import views
5
6urlpatterns = [
7    path('admin/', admin.site.urls),
8    path('', views.index),
9]

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

保存并关闭文件。

最后,您需要更新INSTALLED_APPS,以在settings.py中包含测试站点:

1nano django-apps/testsite/testsite/settings.py
 1[label django-apps/testsite/testsite/settings.py]
 2# ...
 3INSTALLED_APPS = [
 4    'django.contrib.admin',
 5    'django.contrib.auth',
 6    'django.contrib.contenttypes',
 7    'django.contrib.sessions',
 8    'django.contrib.messages',
 9    'django.contrib.staticfiles',
10    'testsite',
11]
12# ...

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

从项目的 root 目录(‘testsite’)启动 Django 开发服务器,使用以下命令,用您自己的服务器的 IP 地址代替 your-server-ip

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

打开一个浏览器,然后访问your-server-ip:8000

Screenshot of the basic view. An image of Sammy the Shark appears in a blue circle. Beneath the image, the text "Hello, Sammy!" appears in blue script.

此时,页面显示了Sammy the Shark的个人资料图像. 图像底下有蓝色脚本的文本 Hello, Sammy!

要停止 Django 开发服务器,点击CONTROL-C

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

第2步:安装CSP中间件

在此步骤中,您将安装并实施 CSP 中间件,以便您可以添加 CSP 标题并在您的视图中使用 CSP 功能。中间件会为任何 Django 请求或响应添加额外的功能。

首先,您将使用pip,Python的包管理器,在Django项目中安装Mozilla的CSP Middleware。 使用以下命令从PyPi,Python Package Index中安装所需的包。 要运行命令,您可以使用CONTROL-C停止Django开发服务器,或者在终端中打开一个新标签:

1pip install django-csp

接下来,将中间件添加到您的 Django 项目的设置中。

1nano testsite/testsite/settings.py

安装了‘django-csp’,您现在可以在‘settings.py’中添加中间软件,这将为您的回复添加CSP标题。 将下列行添加到‘MIDDLEWARE’配置阵列:

 1[label testsite/testsite/settings.py]
 2MIDDLEWARE = [
 3    'csp.middleware.CSPMiddleware',
 4    'django.middleware.security.SecurityMiddleware',
 5    'django.contrib.sessions.middleware.SessionMiddleware',
 6    'django.middleware.common.CommonMiddleware',
 7    'django.middleware.csrf.CsrfViewMiddleware',
 8    'django.contrib.auth.middleware.AuthenticationMiddleware',
 9    'django.contrib.messages.middleware.MessageMiddleware',
10    'django.middleware.clickjacking.XFrameOptionsMiddleware',
11]

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

步骤 3 – 实施 CSP 标题

现在,您的项目支持 CSP,它已经准备好加强安全性。为了实现这一目标,您将配置该项目以将 CSP 标题添加到您的响应中。 CSP 标题是告诉浏览器在遇到特定类型的内容时如何行为的内容。

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

1nano testsite/testsite/settings.py

定义下列变量在文件中的任何地方:

1[label testsite/testsite/settings.py]
2# Content Security Policy
3
4CSP_IMG_SRC = ("'self'")
5
6CSP_STYLE_SRC = ("'self'")
7
8CSP_SCRIPT_SRC = ("'self'")

这些规则是您的 CSP 的锅炉板. 这些行表示分别允许图像、风格表和脚本的来源。 目前,它们都包含自我字符串,这意味着只允许来自您自己的域的资源。

保存并关闭文件,当你完成。

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

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

当你访问‘your-server-ip:8000’,你会看到该网站被打破:

Screenshot of broken project. The image of Sammy does not load and the text, "Hello, Sammy!" appears in default styling (bolded, black text.)

正如预期的那样,图像不会出现,文本会以默认的样式(黑色)出现。这意味着CSP标题正在被执行,我们的页面现在更安全。 因为您之前创建的视图是引用不是您的域的样式表和图像,浏览器会阻止它们。

您的项目现在有一个工作 CSP,该 CSP 告诉浏览器阻止非来自您的域的资源. 接下来,您将修改 CSP 以允许特定资源,从而修复主页的缺失图像和风格。

步骤 4 – 修改 CSP 以允许外部资源

现在你有一个基本的 CSP,你会根据你在网站上使用的内容进行修改。 例如,使用 Adobe 字体和嵌入式 YouTube 视频的网站需要允许这些资源。

您可以使用浏览器的 developer tools来做到这一点。在Inspect Element**中打开Network Monitor**,刷新页面并查看被阻止的资源:

Screenshot showing network monitor. At left, The image of Sammy does not load and the text, "Hello, Sammy!" appears in default styling (bolded, black text). At right, the network monitor shows errors that the styling and image could not load.

网络日志显示,两个资源正在被CSP阻止:一个来自fonts.googleapis.com的风格表和一个来自html.sammy-codes.com的图像。

若要允许来自外部域的资源,请将域添加到匹配文件类型的 CSP 部分,因此,若要允许从 html.sammy-codes.com’ 获取图像,则将 html.sammy-codes.com’ 添加到 CSP_STYLE_SRC

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

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

现在,而不是只允许来自您的域的图像,该网站还允许从html.sammy-codes.com的图像。

索引视图使用 Google 字体. Google 向您的网站提供字体(从 https://fonts.gstatic.com)和风格表来应用它们(从 https://fonts.googleapis.com)。 若要允许字体加载,请将下列内容添加到您的 CSP:

1[label testsite/testsite/settings.py]
2CSP_STYLE_SRC = ("'self'", 'https://fonts.googleapis.com')
3
4CSP_FONT_SRC = ("'self'", 'https://fonts.gstatic.com/')

类似于允许来自html.sammy-codes.com的图像,您还将允许来自fonts.googleapis.com的风格表和来自fonts.gstatic.com的字体。

保存并关闭文件。

<$>[警告] 警告: 类似于自己,还有其他关键字,如不安全的线上,不安全的错误不安全的密码,可以在CSP中使用。

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

现在,Google 字体将被允许在您的网站上加载风格和字体,并且html.sammy-codes.com将被允许加载图像. 但是,当您访问您的服务器上的页面时,您可能会注意到现在只有图像正在加载。

步骤 5 – 使用内线脚本和风格

此时,您已更改 CSP 以允许外部资源,但内部资源,如您的视图中的样式和脚本,仍然不允许。

有两种方式允许内线脚本和风格:内线脚本和哈希. 如果您发现您经常修改内线脚本和风格,请使用内线脚本和风格以避免频繁更改您的 CSP。

使用nonce允许内线脚本

首先,你会使用 nonce 方法. 一个 nonce 是一个随机生成的代币,每个请求都是独一无二的. 如果两个人访问你的网站,他们每个人都会得到一个独特的 nonce 嵌入到你批准的内线脚本和风格中。

要将 nonce 支持添加到您的项目中,您将在settings.py中更新您的 CSP。

1nano testsite/testsite/settings.py

CSP_INCLUDE_NONCE_IN中添加script-srcsettings.py文件中。

定义CSP_INCLUDE_NONCE_IN在文件中的任何地方,并添加script-src:

1[label testsite/testsite/settings.py]
2# Content Security Policy
3
4CSP_INCLUDE_NONCE_IN = ['script-src']

CSP_INCLUDE_NONCE_IN表示您可以添加nonce属性到哪些内行脚本。

保存并关闭文件。

现在允许在视图模板中添加nonce属性时生成内线脚本。

打开index.html来编辑:

1nano testsite/testsite/templates/index.html

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

1[label testsite/testsite/templates/index.html]
2<script>
3    console.log("Hello from the console!");
4</script>

此片段会打印到浏览器控制台的Hello from the console! 但是,由于您的项目有一个 CSP,它只允许内线脚本,如果它们有nonce,这个脚本不会运行,而是会产生错误。

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

Screenshot showing nonce error. At left, the image of Sammy appears above the text, "Hello, Sammy!", which appears in default styling (bolded, black text). At right, the console displays an error message: "Content Security Policy: The page's settings blocked the loading of a resource at inline ("script-src").

图像会加载,因为您在上一步允许了外部资源。正如预期的那样,当前的样式是默认的,因为您尚未允许内线样式。

您可以这样做,将 `nonce="{request.csp_nonce}}" 添加到这个脚本中作为属性。

1[label testsite/testsite/templates/index.html]
2<script nonce="{{request.csp_nonce}}">
3    console.log("Hello from the console!");
4</script>

保存并关闭你的文件,当你完成。

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

Screenshot showing console message. At left, the image of Sammy appears above the text, "Hello, Sammy!", which appears in default styling (bolded, black text). At right, the console displays the message, "Hello from the console!"

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

Screenshot of inspect element, showing the missing nonce. At left, the image of Sammy appears above the text, "Hello, Sammy!", which appears in default styling (bolded, black text). At right, "Inspect element" view shows the HTML of the page, but no value for the nonce appears.

该值不会出现在安全原因. 浏览器已经处理了该值. 它是隐藏的,所以任何可以访问 DOM 的脚本都无法访问它,并将其应用到其他某些脚本。

Screenshot of "View Page Source" with nonce value.

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

这些nonce值在浏览器收到响应时附加到 CSP 标题中:

Screenshot of "Network" tab showing the nonce value. At left, the image of Sammy appears above the text, "Hello, Sammy!", which appears in default styling (bolded, black text). At right, the nonce value appears in the "Network" tab.

浏览器向您的网站提出的每个请求都将为该脚本具有独特的nonce值,因为nonce在CSP标题中提供,这意味着Django服务器批准该特定脚本运行。

例如,您也可以将其应用到风格,通过更新CSP_INCLUDE_NONCE_IN,以允许style-src

使用哈希允许内线风格

允许线性脚本和风格的另一个方法是使用哈希,一个哈希是给定线性资源的唯一标识符。

举个例子,这是我们模板中的内线风格:

 1[label testsite/testsite/templates/index.html]
 2<style>
 3    h1 {
 4        font-family: "Yellowtail", cursive;
 5        margin: 0.5em 0 0 0;
 6        color: #0069ff;
 7        font-size: 4em;
 8        line-height: 0.6;
 9    }
10
11    img {
12        border-radius: 100%;
13        border: 6px solid #0069ff;
14    }
15
16    .center {
17        text-align: center;
18        position: absolute;
19        top: 50vh;
20        left: 50vw;
21        transform: translate(-50%, -50%);
22    }
23</style>

当您在浏览器中查看网站时,图像会成功加载,但没有应用字体和风格:

Screenshot showing broken styling. An image of Sammy appears above the text, "Hello, Sammy!", which renders in default styling (bolded, black text).

在浏览器的控制台中,你会发现一个错误,即一个内线风格违反CSP(可能有其他错误,但寻找有关内线风格的错误)。

Screenshot of DevTools Error: "Refused to apply inline style because it violates the following Content Security Policy directive: "style-src 'self' https://fonts.googleapis.com". Either the 'unsafe-inline' keyword, a hash, or a nonce is required to enable inline execution."

这个错误是因为我们的 CSP 未批准该风格,但请注意,该错误提供了批准该风格片段所需的哈希,此哈希是该特定风格片段独一无二的,没有其他片段将有相同的哈希。当此哈希放在 CSP 中时,每次加载此特定风格时,它将被批准。

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

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

sha256-...哈希添加到CSP_STYLE_SRC列表将允许浏览器在没有任何错误的情况下加载风格表。

保存和关闭文件。

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

Screenshot showing styles applied. An image of Sammy the Shark appears in a blue circle. Beneath the image, the text "Hello, Sammy!" appears in blue script.

在此步骤中,您使用了两种不同的方法,即Nonces和Hashes,以允许Inline风格和脚本。

但是,有一个重要的问题要解决。 CSP 很难维护,特别是对于大型网站,您可能需要一种方法来跟踪 CSP 阻止资源时,以便您可以确定它是否是一个恶意资源或只是您网站的破坏部分。

步骤 6 – 使用 Sentry 报告违规行为(可选)

鉴于 CSP 往往是多么严格,所以知道何时阻止内容是好事,特别是因为阻止内容可能意味着您的网站上的某些功能不会起作用。

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

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

Screenshot of Sentry UI.

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

Screenshot showing Sentry UI for creating a project.

您将看到一系列标志,标题表示 **选择平台。**选择 Django:

Screenshot showing Sentry UI for selecting a Django project.

然后,在底部,命名您的项目(对于这个示例,我们将使用sammys-tutorial),然后点击创建项目按钮:

Screenshot showing Sentry UI for creating a new project.

Sentry 会给你一个代码片段来添加到你的 settings.py 文件. 保存此片段来添加在稍后一步。

在您的终端上,安装Sentry SDK:

1pip install --upgrade sentry-sdk

打开settings.py如下:

1nano testsite/testsite/settings.py

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

 1[label testsite/testsite/settings.py]
 2import sentry_sdk
 3from sentry_sdk.integrations.django import DjangoIntegration
 4
 5sentry_sdk.init(
 6    dsn="SENTRY_DSN",
 7    integrations=[DjangoIntegration()],
 8
 9    # Set traces_sample_rate to 1.0 to capture 100%
10    # of transactions for performance monitoring.
11    # We recommend adjusting this value in production.
12    traces_sample_rate=1.0,
13
14    # If you wish to associate users to errors (assuming you are using
15    # django.contrib.auth) you may enable sending PII data.
16    send_default_pii=True
17)

此代码由Sentry提供,以便它可以记录在您的应用程序中发生的任何错误。 这是Sentry的默认配置,并将Sentry初始化用于在我们的服务器上记录问题。 技术上,您不需要在您的服务器上初始化Sentry,因为CSP违反,但在罕见的情况下有某些问题 rendering nonces或哈希,这些错误将被登录到Sentry。

保存并关闭文件。

接下来,回到您的项目的仪表板,然后点击齿轮图标进入 设置:

Screenshot showing Sentry UI for project settings.

点击安全标题标签:

Screenshot showing Sentry UI for Security Headers in project settings.

复制报告-uri :

Screenshot showing Sentry UI for copying the report URI in project settings.

将其添加到您的 CSP 如下:

1[label testsite/testsite/settings.py]
2# Content Security Policy
3
4CSP_REPORT_URI = "your-report-uri"

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

保存并关闭您的文件. 现在,当CSP执法导致违规时,Sentry会将其登录到此URI中. 您可以尝试通过从CSP中删除域或哈希,或从您先前添加的脚本中删除nonce。 在浏览器中加载页面,您将在Sentry的 Issues页面中看到错误:

Screenshot showing Sentry UI for error logs.If you find you are overwhelmed by the number of logs, you can also define CSP_REPORT_PERCENTAGE in settings.py to only send a percentage of the logs to Sentry.

1[label testsite/testsite/settings.py]
2# Content Security Policy
3# Send 10% of the logs to Sentry
4CSP_REPORT_PERCENTAGE = 0.1

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

结论

在本文中,您通过内容安全策略保护了 Django 应用程序. 您更新了您的策略以允许外部资源,并使用 nonces 和 hashes 来允许内线 scropts 和风格. 您还将其配置为向 Sentry 发送违规行为。

Published At
Categories with 技术
comments powered by Disqus