如何使用内容安全策略保护Django应用程序
作为 Write for DOnations 计划的一部分,作者选择了 Girls Who Code 来接受捐赠。
介绍
当您访问一个网站时,会使用各种资源来加载和呈现它。 例如,当您转到 https://www.digitalocean.com
时,您的浏览器会直接从 digitalocean.com
下载 HTML 和 CSS。 但是,图像和其他资产是从 assets.digitalocean.com
下载的,分析脚本是从它们各自的域加载的。
一些网站使用多种不同的服务、样式和脚本来加载和呈现其内容,您的浏览器将执行所有这些。 浏览器不知道代码是否是恶意的,因此开发人员有责任保护用户。 因为网站上可能有很多资源,所以在浏览器中设置一个只允许获得批准的资源的功能是确保用户不会受到损害的好方法。 这就是内容安全策略 (CSP) 的用途。
使用 CSP 标头,开发人员可以明确允许某些资源运行,同时阻止所有其他资源。 因为大多数站点可以拥有超过 100 个资源,并且每个资源都必须获得特定资源类别的批准,因此实施 CSP 可能是一项乏味的任务。 但是,具有 CSP 的网站会更安全,因为它确保只允许运行经过批准的资源。
在本教程中,您将在基本的 Django 应用程序中实现 CSP。 您将自定义 CSP 以允许某些域和内联资源运行。 或者,您还可以使用 Sentry 来记录违规行为。
先决条件
要完成本教程,您需要:
- 在本地计算机或 DigitalOcean Droplet 上运行的 Django 项目(首选版本 3 或更高版本)。 如果您没有,可以使用教程 如何在 Ubuntu 20.04 上安装 Django 并设置开发环境来创建一个。
- 像 Firefox 或 Chrome 这样的网络浏览器,并了解浏览器网络工具。 有关使用浏览器网络工具的更多信息,请查看 Firefox 中的 Network Monitor 或 Chrome 中的 DevTools Network Tab 的产品文档。 有关浏览器开发工具的更多一般指南,请参阅指南:什么是浏览器开发工具?
- Python 3 和 Django 知识,您可以从教程系列 如何在 Python 和 Django 开发 中获得这些知识。
- Sentry 上用于跟踪 CSP 违规的帐户(可选)。
第 1 步 — 制作演示视图
在此步骤中,您将修改应用程序处理视图的方式,以便添加 CSP 支持。
作为先决条件,您安装了 Django 并设置了一个示例项目。 Django 中的默认视图过于简单,无法演示 CSP 中间件的所有功能,因此您将为本教程创建一个简单的 HTML 页面。
导航到您在先决条件中创建的项目文件夹:
cd django-apps
在 django-apps
目录中,创建您的虚拟环境。 我们将其称为通用的 env
,但您应该使用对您和您的项目有意义的名称。
virtualenv env
现在,使用以下命令激活虚拟环境:
. env/bin/activate
在虚拟环境中,使用 nano
或您喜欢的文本编辑器在您的项目文件夹中创建一个 views.py
文件:
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
:
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), ]
您刚刚创建的新视图现在可以在您访问 /
时查看(当应用程序运行时)。
保存并关闭文件。
最后,您需要更新 INSTALLED_APPS
以在 settings.py
中包含 testsite
:
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', ] # ...
在这里,您将 testsite
添加到 settings.py
中的应用程序列表中,以便 Django 可以对您的项目结构做出一些假设。 在这种情况下,它将假定 templates
文件夹包含可用于呈现视图的 Django 模板。
在项目的根目录 (testsite
) 中,使用以下命令启动 Django 开发服务器,将 your-server-ip
替换为您自己服务器的 IP 地址。
cd ~/django-apps/testsite python manage.py runserver your-server-ip:8000
打开浏览器并访问 your-server-ip:8000
。 该页面应如下所示:
此时,页面显示鲨鱼萨米的头像。 图片下方是蓝色字体的文本 Hello, Sammy!。
要停止 Django 开发服务器,请点击 CONTROL-C
。
在这一步中,您创建了一个基本视图,作为 Django 项目的主页。 接下来,您将向应用程序添加 CSP 支持。
第 2 步 — 安装 CSP 中间件
在此步骤中,您将安装并实现 CSP 中间件,以便您可以添加 CSP 标头并在视图中使用 CSP 功能。 中间件向 Django 处理的任何请求或响应添加附加功能。 在这种情况下,Django-CSP 中间件 将 CSP 支持添加到 Django 响应中。
首先,您将使用 Python 的包管理器 pip
在 Django 项目中安装 Mozilla 的 CSP 中间件。 使用以下命令从 PyPi(Python 包索引)安装必要的包。 要运行该命令,您可以使用 CONTROL-C
停止 Django 开发服务器或在终端中打开一个新选项卡:
pip install django-csp
接下来,将中间件添加到 Django 项目的设置中。 打开settings.py
:
nano testsite/testsite/settings.py
安装 django-csp
后,您现在可以在 settings.py
中添加中间件。 这会将 CSP 标头添加到您的响应中。 将以下行添加到 MIDDLEWARE
配置数组:
测试站点/测试站点/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 标头。
第 3 步 — 实施 CSP 标头
现在您的项目支持 CSP,它已准备好进行安全强化。 为此,您将配置项目以将 CSP 标头添加到您的响应中。 CSP 标头告诉浏览器在遇到特定类型的内容时如何表现。 因此,如果标题说只允许来自特定域的图像,那么浏览器将只允许来自该域的图像。
使用 nano 或您喜欢的文本编辑器,打开 settings.py
:
nano testsite/testsite/settings.py
在文件的任意位置定义以下变量:
测试站点/测试站点/settings.py
# Content Security Policy CSP_IMG_SRC = ("'self'") CSP_STYLE_SRC = ("'self'") CSP_SCRIPT_SRC = ("'self'")
这些规则是 CSP 的样板。 这些行分别指示图像、样式表和脚本允许哪些来源。 现在,它们都包含字符串 'self'
,这意味着只允许来自您自己域的资源。
完成后保存并关闭文件。
使用以下命令运行 Django 项目:
python manage.py runserver your-server-ip:8000
当您访问 your-server-ip:8000
时,您会看到该站点已损坏:
正如预期的那样,图像没有出现,文本以默认样式显示(粗体黑色)。 这意味着 CSP 标头正在被强制执行,我们的页面现在更加安全。 因为您之前创建的视图引用了不属于您自己的域的样式表和图像,所以浏览器会阻止它们。
您的项目现在有一个有效的 CSP,它告诉浏览器阻止不来自您的域的资源。 接下来,您将修改 CSP 以允许特定资源,这将修复主页缺少的图像和样式。
第 4 步 - 修改 CSP 以允许外部资源
现在您已经有了一个基本的 CSP,您将根据您在站点上使用的内容对其进行修改。 例如,使用 Adobe Fonts 和嵌入式 YouTube 视频的网站将需要允许这些资源。 但是,如果您的网站仅显示您自己域内的图像,您可以将图像设置保留为限制性默认值。
第一步是找到您需要批准的所有资源。 您可以使用浏览器的 开发者工具 来执行此操作。 打开Inspect Element中的Network Monitor,刷新页面,查看被阻塞的资源:
网络日志显示 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
变量中:
测试站点/测试站点/settings.py
CSP_IMG_SRC = ("'self'", 'https://html.sammy-codes.com')
现在,该站点不仅允许来自您的域的图像,还允许来自 html.sammy-codes.com
的图像。
索引视图使用 Google 字体。 Google 为您的网站提供字体(来自 https://fonts.gstatic.com
)和应用它们的样式表(来自 https://fonts.googleapis.com
)。 要允许加载字体,请将以下内容添加到您的 CSP:
测试站点/测试站点/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-inline
、unsafe-eval
或 unsafe-hashes
等其他关键字可用于太阳能电池板。 强烈建议您避免在 CSP 中使用这些规则。 虽然这些将使实施更容易,但它们可用于规避 CSP 并使其无用。
有关详细信息,请参阅 Mozilla 产品文档中的“不安全内联脚本”。
现在,Google Fonts 将被允许在您的网站上加载样式和字体,并且 html.sammy-codes.com
将被允许加载图像。 但是,当您访问服务器上的页面时,您可能会注意到现在只有图像正在加载。 这是因为 HTML 中用于应用字体的内联样式是不允许的。 您将在下一步中解决该问题。
第 5 步 — 使用内联脚本和样式
此时,您已修改 CSP 以允许外部资源。 但是内联资源,例如你视图中的样式和脚本,仍然是不允许的。 在此步骤中,您将让那些工作正常,以便您可以应用字体样式。
有两种方法可以允许内联脚本和样式:随机数和散列。 如果您发现您经常修改内联脚本和样式,请使用 nonce 以避免频繁更改您的 CSP。 如果您很少更新内联脚本和样式,则使用哈希是一种合理的方法。
使用 nonce
允许内联脚本
首先,您将使用 nonce 方法。 nonce 是随机生成的令牌,对每个请求都是唯一的。 如果有两个人访问您的站点,他们每个人都会得到一个独特的 nonce
,它嵌入在您批准的内联脚本和样式中。 将 nonce 视为一次性密码,它允许站点的某些部分在单个会话中运行。
要为您的项目添加 nonce 支持,您将在 settings.py
中更新您的 CSP。 打开文件进行编辑:
nano testsite/testsite/settings.py
在settings.py
文件的CSP_INCLUDE_NONCE_IN
中添加script-src
。
在文件的任意位置定义 CSP_INCLUDE_NONCE_IN
并将 'script-src'
添加到它:
测试站点/测试站点/settings.py
# Content Security Policy CSP_INCLUDE_NONCE_IN = ['script-src']
CSP_INCLUDE_NONCE_IN
表示您可以向哪些内联脚本添加 nonce
属性。 CSP_INCLUDE_NONCE_IN
作为数组处理,因为多个数据源支持随机数(例如,style-src
)。
保存并关闭文件。
现在,当您在视图模板中将 nonce
属性添加到它们时,允许为内联脚本生成随机数。 要尝试这一点,您将使用一个简单的 JavaScript 片段。
打开index.html
进行编辑:
nano testsite/testsite/templates/index.html
在 HTML 的 <head>
中添加以下代码段:
测试站点/测试站点/模板/index.html
<script> console.log("Hello from the console!"); </script>
此代码段将 Hello from the console!"
打印到浏览器的控制台。 但是,由于您的项目有一个 CSP,如果它们具有 nonce
,则仅允许内联脚本,因此该脚本将不会运行,而是会产生错误。
刷新页面时,您可以在浏览器的控制台中看到此错误:
加载图像是因为您在上一步中允许使用外部资源。 正如预期的那样,样式当前是默认设置,因为您还没有允许内联样式。 同样如预期的那样,控制台消息没有打印并返回错误。 你需要给它一个 nonce
来批准它。
您可以通过将 nonce="模板:Request.csp nonce"
作为属性添加到此脚本来做到这一点。 打开 index.html
进行编辑并添加突出显示的部分,如下所示:
测试站点/测试站点/模板/index.html
<script nonce="{{request.csp_nonce}}"> console.log("Hello from the console!"); </script>
完成后保存并关闭文件。
如果刷新页面,脚本现在将执行:
当您查看 Inspect Element 时,您会注意到该属性没有值:
出于安全原因,该值不会出现。 浏览器已经处理了这个值。 它是隐藏的,因此任何可以访问 DOM 的脚本都无法访问它并将其应用于其他脚本。 如果您改为 查看页面源 ,浏览器会收到以下内容:
请注意,每次刷新页面时,nonce
的值都会发生变化。 这是因为我们项目中的 CSP 中间件会为每个请求生成一个新的 nonce
。
当浏览器收到响应时,这些 nonce
值会附加到 CSP 标头:
浏览器向您的站点发出的每个请求都将具有该脚本的唯一 nonce
值。 由于 CSP 标头中提供了 nonce
,这意味着 Django 服务器批准了该特定脚本的运行。
您已更新您的项目以使用 nonce,它可以应用于多个资源。 例如,您也可以将其应用于样式,方法是将 CSP_INCLUDE_NONCE_IN
更新为允许 style-src
。 但是有一种更简单的方法来批准内联资源,这就是您接下来要做的。
使用散列允许内联样式
允许内联脚本和样式的另一种方法是使用哈希。 哈希是给定内联资源的唯一标识符。
例如,这是我们模板中的内联样式:
测试站点/测试站点/模板/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
来应用哈希,如下所示:
nano testsite/testsite/settings.py
测试站点/测试站点/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 仪表板的左上角,单击 Projects 选项卡:
在右上角,点击【X38X】创建项目【X56X】按钮:
您将看到许多标题为 Choose a platform. 选择 Django 的徽标:
然后,在底部,为您的项目命名(在本例中,我们将使用 sammys-tutorial
),然后单击 Create Project 按钮:
Sentry 会给你一个代码片段来添加到你的 settings.py
文件中。 保存此代码段以在以后的步骤中添加。
在您的终端中,安装 Sentry SDK:
pip install --upgrade sentry-sdk
像这样打开 settings.py
:
nano testsite/testsite/settings.py
将以下内容添加到文件末尾,并确保将 SENTRY_DSN
替换为仪表板中的值:
测试站点/测试站点/settings.py
import sentry_sdk from sentry_sdk.integrations.django import DjangoIntegration sentry_sdk.init( dsn="SENTRY_DSN", integrations=[DjangoIntegration()], # Set traces_sample_rate to 1.0 to capture 100% # of transactions for performance monitoring. # We recommend adjusting this value in production. traces_sample_rate=1.0, # If you wish to associate users to errors (assuming you are using # django.contrib.auth) you may enable sending PII data. send_default_pii=True )
此代码由 Sentry 提供,以便它可以记录应用程序中发生的任何错误。 它是 Sentry 的默认配置,并初始化 Sentry 以在我们的服务器上记录问题。 从技术上讲,您不需要在服务器上初始化 Sentry 以防止违反 CSP,但在极少数情况下存在渲染 nonce 或散列的问题,这些错误将记录到 Sentry。
保存并关闭文件。
接下来,返回项目的仪表板并单击齿轮图标进入 设置 :
转到 安全标头 选项卡:
复制 report-uri
:
像这样将它添加到您的 CSP 中:
测试站点/测试站点/settings.py
# Content Security Policy CSP_REPORT_URI = "your-report-uri"
请务必将 your-report-uri
替换为您从仪表板复制的值。
保存并关闭您的文件。 现在,当 CSP 强制导致违规时,Sentry 会将其记录到此 URI。 您可以通过从 CSP 中删除域或哈希,或从您之前添加的脚本中删除 nonce
来尝试此操作。 在浏览器中加载页面,你会在 Sentry 的 Issues 页面看到错误:
如果你发现日志的数量让你不堪重负,你也可以在 settings.py
中定义 CSP_REPORT_PERCENTAGE
只发送一定百分比的日志给 Sentry。
测试站点/测试站点/settings.py
# Content Security Policy # Send 10% of the logs to Sentry CSP_REPORT_PERCENTAGE = 0.1
现在,只要有 CSP 违规,您就会收到通知并可以在 Sentry 中查看错误。
结论
在本文中,您使用内容安全策略保护了 Django 应用程序。 您更新了策略以允许外部资源,并使用随机数和散列来允许内联脚本和样式。 您还将其配置为将违规发送到 Sentry。 下一步,请查看 Django CSP 文档 以了解有关如何实施 CSP 的更多信息。