コンテンツセキュリティポリシーでDjangoアプリケーションを保護する方法
序章
Webサイトにアクセスすると、さまざまなリソースを使用してWebサイトをロードおよびレンダリングします。 例として、あなたが行くとき https://www.digitalocean.com
、ブラウザはHTMLとCSSをから直接ダウンロードします digitalocean.com
. ただし、画像やその他のアセットはからダウンロードされます assets.digitalocean.com
、および分析スクリプトは、それぞれのドメインからロードされます。
一部のWebサイトは、多数の異なるサービス、スタイル、およびスクリプトを使用してコンテンツをロードおよびレンダリングし、ブラウザーはそれらすべてを実行します。 ブラウザはコードが悪意のあるものかどうかを認識しないため、ユーザーを保護するのは開発者の責任です。 Webサイトには多くのリソースが存在する可能性があるため、承認されたリソースのみを許可する機能をブラウザーに含めることは、ユーザーが危険にさらされないようにするための良い方法です。 これが、コンテンツセキュリティポリシー(CSP)の目的です。
開発者は、CSPヘッダーを使用して、特定のリソースの実行を明示的に許可し、他のすべてのリソースを防止することができます。 ほとんどのサイトは100以上のリソースを持つことができ、各サイトは特定のカテゴリのリソースに対して承認される必要があるため、CSPの実装は面倒な作業になる可能性があります。 ただし、CSPを使用するWebサイトは、承認されたリソースのみの実行が許可されるため、より安全になります。
このチュートリアルでは、基本的なDjangoアプリケーションにCSPを実装します。 CSPをカスタマイズして、特定のドメインとインラインリソースを実行できるようにします。 オプションで、Sentryを使用して違反をログに記録することもできます。
前提条件
このチュートリアルを完了するには、次のものが必要です。
- ローカルマシンまたはDigitalOceanDropletのいずれかで動作するDjangoプロジェクト(バージョン3以降が推奨されます)。 お持ちでない場合は、チュートリアル Ubuntu20.04でDjangoをインストールして開発環境をセットアップする方法を使用して作成できます。
- FirefoxやChromeなどのWebブラウザーと、ブラウザーネットワークツールの理解。 ブラウザネットワークツールの使用の詳細については、FirefoxのネットワークモニターまたはChromeのDevToolsネットワークタブの製品ドキュメントを確認してください。 ブラウザ開発者ツールのより一般的なガイダンスについては、ガイドを参照してください:ブラウザ開発者ツールとは何ですか?
- チュートリアルシリーズPythonでのコーディング方法およびDjango開発から得られるPython3およびDjangoの知識。
- CSP違反を追跡するためのSentryのアカウント(オプション)。
ステップ1—デモビューを作成する
このステップでは、CSPサポートを追加できるように、アプリケーションがビューを処理する方法を変更します。
前提条件として、Djangoをインストールし、サンプルプロジェクトをセットアップしました。 Djangoのデフォルトのビューは単純すぎて、CSPミドルウェアのすべての機能を示すことができないため、このチュートリアル用の単純なHTMLページを作成します。
前提条件で作成したプロジェクトフォルダに移動します。
- cd django-apps
中にいる間 django-apps
ディレクトリ、仮想環境を作成します。 これをジェネリックと呼びます env
、ただし、自分とプロジェクトにとって意味のある名前を使用する必要があります。
- virtualenv env
次に、次のコマンドを使用して仮想環境をアクティブ化します。
- . env/bin/activate
仮想環境内で、 views.py
を使用してプロジェクトフォルダ内のファイル nano
、またはお気に入りのテキストエディタ:
- nano django-apps/testsite/testsite/views.py
次に、レンダリングする基本的なビューを追加します index.html
次に作成するテンプレート。 以下を追加します views.py
:
from django.shortcuts import render
def index(request):
return render(request, "index.html")
完了したら、ファイルを保存して閉じます。
作成する index.html
新しいテンプレート templates
ディレクトリ:
mkdir django-apps/testsite/testsite/templates
nano django-apps/testsite/testsite/templates/index.html
以下を追加します 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!とSammytheSharkの画像が表示されます。
完了したら、ファイルを保存して閉じます。
このビューにアクセスするには、更新する必要があります urls.py
:
- nano django-apps/testsite/testsite/urls.py
をインポートします views.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
含める testsite
の settings.py
:
- nano 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ミドルウェアはDjango応答にCSPサポートを追加します。
まず、MozillaのCSPミドルウェアをDjangoプロジェクトにインストールします。 pip
、Pythonのパッケージマネージャー。 次のコマンドを使用して、PythonPackageIndexであるPyPiから必要なパッケージをインストールします。 コマンドを実行するには、を使用してDjango開発サーバーを停止することができます CONTROL-C
または、ターミナルで新しいタブを開きます。
- pip install django-csp
次に、ミドルウェアをDjangoプロジェクトの設定に追加します。 開ける settings.py
:
- nano testsite/testsite/settings.py
と django-csp
インストールすると、ミドルウェアをに追加できるようになります settings.py
. これにより、応答にCSPヘッダーが追加されます。 次の行をに追加します MIDDLEWARE
構成アレイ:
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
ファイル内の任意の場所で次の変数を定義します。
# 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ができたので、サイトで使用しているものに基づいてCSPを変更します。 例として、Adobeフォントと埋め込まれたYouTubeビデオを使用するWebサイトは、これらのリソースを許可する必要があります。 ただし、Webサイトに自分のドメイン内の画像のみが表示される場合は、画像設定を制限付きのデフォルトのままにしておくことができます。
最初のステップは、承認する必要のあるすべてのリソースを見つけることです。 これを行うには、ブラウザの開発者ツールを使用できます。 InspectElementでNetworkMonitor を開き、ページを更新して、ブロックされたリソースを確認します。
ネットワークログは、2つのリソースが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
変数:
CSP_IMG_SRC = ("'self'", 'https://html.sammy-codes.com')
現在、ドメインからの画像のみを許可するのではなく、サイトはからの画像も許可しています html.sammy-codes.com
.
インデックスビューはGoogleFontsを使用しています。 Googleはあなたのサイトにフォントを提供します( https://fonts.gstatic.com
)そしてそれらを適用するためのスタイルシート付き(から https://fonts.googleapis.com
). フォントをロードできるようにするには、CSPに以下を追加します。
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でこれらのルールを使用しないことを強くお勧めします。 これらは実装を容易にしますが、CSPを回避して役に立たないようにするために使用できます。
詳細については、「安全でないインラインスクリプト」に関するMozilla製品のドキュメントを参照してください。
これで、GoogleFontsはサイトにスタイルとフォントをロードできるようになります。 html.sammy-codes.com
画像の読み込みが許可されます。 ただし、サーバー上のページにアクセスすると、現在画像のみが読み込まれていることに気付く場合があります。 これは、フォントの適用に使用されるHTMLのインラインスタイルが許可されていないためです。 次のステップで修正します。
ステップ5—インラインスクリプトとスタイルの操作
この時点で、外部リソースを許可するようにCSPを変更しました。 ただし、ビュー内のスタイルやスクリプトなどのインラインリソースは引き続き許可されていません。 このステップでは、フォントのスタイルを適用できるようにそれらを機能させます。
インラインスクリプトとスタイルを許可するには、ナンスとハッシュの2つの方法があります。 インラインスクリプトとスタイルを頻繁に変更していることがわかった場合は、ナンスを使用してCSPが頻繁に変更されないようにします。 インラインスクリプトとスタイルをめったに更新しない場合は、ハッシュを使用するのが妥当なアプローチです。
使用する nonce
インラインスクリプトを許可するには
まず、ナンスアプローチを使用します。 ナンスは、各リクエストに固有のランダムに生成されたトークンです。 2人があなたのサイトにアクセスすると、それぞれがユニークになります nonce
これは、承認するインラインスクリプトとスタイルに埋め込まれています。 nonceは、サイトの特定の部分を1回のセッションで実行することを承認するワンタイムパスワードと考えてください。
プロジェクトにナンスサポートを追加するには、次の場所でCSPを更新します。 settings.py
. 編集用にファイルを開きます。
- nano testsite/testsite/settings.py
追加 script-src
の CSP_INCLUDE_NONCE_IN
の中に settings.py
ファイル。
定義 CSP_INCLUDE_NONCE_IN
ファイルの任意の場所に追加します 'script-src'
それに:
# 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
次のスニペットをに追加します <head>
HTMLの:
<script>
console.log("Hello from the console!");
</script>
このスニペットは印刷されます Hello from the console!"
ブラウザのコンソールに。 ただし、プロジェクトにはCSPがあるため、インラインスクリプトが許可されているのは、 nonce
、このスクリプトは実行されず、代わりにエラーが発生します。
ページを更新すると、ブラウザのコンソールに次のエラーが表示されます。
前の手順で外部リソースを許可したため、画像が読み込まれます。 予想どおり、インラインスタイルをまだ許可していないため、現在、スタイルはデフォルトです。 また、予想どおり、コンソールメッセージは出力されず、エラーが返されました。 あなたはそれに与える必要があります nonce
それを承認します。
あなたは追加することによってそれを行うことができます nonce="{{request.csp_nonce}}"
このスクリプトに属性として追加します。 開ける index.html
ここに示すように、編集して強調表示された部分を追加します。
<script nonce="{{request.csp_nonce}}">
console.log("Hello from the console!");
</script>
完了したら、ファイルを保存して閉じます。
ページを更新すると、スクリプトが実行されます。
Inspect Element を見ると、属性に値がないことがわかります。
セキュリティ上の理由から、この値は表示されません。 ブラウザはすでに値を処理しています。 DOMにアクセスできるスクリプトがアクセスして他のスクリプトに適用できないように、非表示になっています。 代わりにページソースを表示する場合、これはブラウザが受け取ったものです。
ページを更新するたびに、 nonce
値が変わります。 これは、プロジェクトのCSPミドルウェアが新しいを生成するためです nonce
リクエストごとに。
これらは nonce
ブラウザが応答を受信すると、値がCSPヘッダーに追加されます。
ブラウザがサイトに対して行うすべてのリクエストには、固有のものがあります nonce
そのスクリプトの値。 以来 nonce
はCSPヘッダーで提供されます。これは、Djangoサーバーがその特定のスクリプトの実行を承認したことを意味します。
複数のリソースに適用できるnonceで動作するようにプロジェクトを更新しました。 たとえば、更新することでスタイルにも適用できます CSP_INCLUDE_NONCE_IN
許可する style-src
. ただし、インラインリソースを承認するためのより簡単なアプローチがあり、それが次に行うことです。
ハッシュを使用してインラインスタイルを許可する
インラインスクリプトとスタイルを許可するための別のアプローチは、ハッシュを使用することです。 ハッシュは、特定のインラインリソースの一意の識別子です。
例として、これはテンプレートのインラインスタイルです。
<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で古いハッシュを置き換える必要があります。
次に、ハッシュをに追加して適用します CSP_STYLE_SRC
の settings.py
、 そのようです:
- nano testsite/testsite/settings.py
CSP_STYLE_SRC = ("'self' 'sha256-r5bInLZB0y6ZxHFpmz7cjyYrndjwCeDLDu/1KeMikHA='", 'https://fonts.googleapis.com')
追加する sha256-...
ハッシュ CSP_STYLE_SRC
listを使用すると、ブラウザはエラーなしでスタイルシートをロードできます。
ファイルを保存して閉じます。
ここで、ブラウザにサイトをリロードすると、フォントとスタイルが正常にロードされます。
インラインスタイルとスクリプトが正しく機能するようになりました。 このステップでは、2つの異なるアプローチ、ナンスとハッシュを使用して、インラインスタイルとスクリプトを許可しました。
しかし、取り組むべき重要な問題があります。 CSPは、特に大きなWebサイトの場合、維持するのが面倒です。 CSPがリソースをブロックするタイミングを追跡して、それが悪意のあるリソースなのか、単にサイトの一部が壊れているのかを判断できるようにする方法が必要になる場合があります。 次のステップでは、Sentryを使用して、CSPによって生成されたすべての違反をログに記録して追跡します。
ステップ6—歩哨による違反の報告(オプション)
CSPがどれほど厳格になる傾向があるかを考えると、コンテンツをブロックしている時期を知っておくとよいでしょう。特に、コンテンツをブロックすると、サイトの一部の機能が機能しなくなる可能性があるためです。 Sentry などのツールは、CSPがユーザーの要求をブロックしていることを通知できます。 このステップでは、CSP違反をログに記録して報告するようにSentryを構成します。
前提条件として、Sentryのアカウントにサインアップしました。 次に、プロジェクトを作成します。
Sentryダッシュボードの左上隅で、プロジェクトタブをクリックします。
右上隅にあるプロジェクトの作成ボタンをクリックします。
プラットフォームを選択してください。Djangoを選択してくださいというタイトルのロゴがいくつか表示されます。
次に、下部でプロジェクトに名前を付けます(この例では、 sammys-tutorial
)、プロジェクトの作成ボタンをクリックします。
Sentryは、あなたに追加するコードスニペットを提供します settings.py
ファイル。 このスニペットを保存して、後のステップで追加します。
ターミナルに、SentrySDKをインストールします。
- pip install --upgrade sentry-sdk
開ける settings.py
そのようです:
- nano testsite/testsite/settings.py
ファイルの最後に以下を追加し、必ず置き換えてください SENTRY_DSN
ダッシュボードからの値で:
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を初期化します。 技術的には、CSP違反のためにサーバーでSentryを初期化する必要はありませんが、まれに、ナンスまたはハッシュのレンダリングで問題が発生した場合、これらのエラーはSentryに記録されます。
ファイルを保存して閉じます。
次に、プロジェクトのダッシュボードに戻り、歯車のアイコンをクリックして設定に移動します。
セキュリティヘッダータブに移動します。
をコピーします report-uri
:
次のようにCSPに追加します。
# Content Security Policy
CSP_REPORT_URI = "your-report-uri"
必ず交換してください your-report-uri
ダッシュボードからコピーした値を使用します。
ファイルを保存して閉じます。 これで、CSPの適用によって違反が発生した場合、SentryはそれをこのURIに記録します。 CSPからドメインまたはハッシュを削除するか、 nonce
以前に追加したスクリプトから。 ブラウザにページをロードすると、SentryのIssueページにエラーが表示されます。
ログの数に圧倒されている場合は、次のように定義することもできます CSP_REPORT_PERCENTAGE
の settings.py
ログの一部のみをSentryに送信します。
# Content Security Policy
# Send 10% of the logs to Sentry
CSP_REPORT_PERCENTAGE = 0.1
これで、CSP違反が発生するたびに通知が届き、Sentryでエラーを表示できます。
結論
この記事では、コンテンツセキュリティポリシーを使用してDjangoアプリケーションを保護しました。 ポリシーを更新して外部リソースを許可し、ナンスとハッシュを使用してインラインスクリプトとスタイルを許可しました。 また、Sentryに違反を送信するように構成しました。 次のステップとして、 Django CSPドキュメントをチェックして、CSPを適用する方法の詳細を確認してください。