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

作者選擇Girls Who Code作為Write for DOnations計劃的捐贈對象。

介紹

當您訪問網站時,會使用各種資源來加載和呈現它。舉例來說,當您訪問https://www.digitalocean.com時,您的瀏覽器直接從digitalocean.com下載HTML和CSS。然而,圖像和其他資產則從assets.digitalocean.com下載,而分析腳本則從它們各自的域名中加載。

一些網站使用多種不同的服務、樣式和腳本來加載和呈現它們的內容,您的瀏覽器將執行所有這些內容。瀏覽器不知道代碼是否惡意,因此保護用戶是開發人員的責任。因為一個網站上可能有許多資源,因此在瀏覽器中具有僅允許批准資源的功能是確保用戶不受威脅的一種好方法。這就是內容安全政策(CSP)的用途。

使用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,Python 的套件管理器,在您的 Django 專案中安裝 Mozilla 的 CSP Middleware。使用以下命令從 PyPi,Python 套件索引中安裝必要的套件。要運行該命令,您可以停止 Django 開發服務器,使用 CONTROL-C,或在終端中打開新標籤:

  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 標頭。

步驟 3 — 實現 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 Fonts和嵌入式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字体。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字体将被允许在您的站点上加载样式和字体,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被处理为数组。

保存并关闭文件。

现在允许为内联脚本生成非ces,当您在视图模板中为其添加nonce属性时。要尝试此操作,您将使用一个简单的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,因此此脚本将无法运行,而是会产生错误。

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

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

您可以通过将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 中時,每次加載此特定樣式時都會被批准。但是,如果您修改了這些樣式,您需要獲取新的哈希並將舊的替換為它。

現在,您將通過將其添加到 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 列表將允許瀏覽器加載樣式表而不會產生任何錯誤。

保存並關閉文件。

現在,在瀏覽器中重新加載網站,字型和樣式應該能夠成功加載:

內聯樣式和腳本現在正常運作。在這一步驟中,您使用了兩種不同的方法,即nonce和哈希,以允許內聯樣式和腳本。

但是,有一個重要的問題需要解決。尤其對於大型網站來說,CSPs很難維護。您可能需要一種方法來跟踪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違規,但是在極少數情況下,如果存在某些無法渲染nonce或哈希的問題,這些錯誤將被記錄到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應用程序。您更新了策略以允許外部資源,並使用nonce和哈希來允許內嵌的腳本和樣式。您還將其配置為將違規情況發送到Sentry。作為下一步,請查看Django CSP文件以了解如何強制執行您的CSP。

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