コンテンツセキュリティポリシーでDjangoアプリケーションを保護する方法
著者は、 Write for DOnations プログラムの一環として、 Girls WhoCodeを選択して寄付を受け取りました。
序章
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
仮想環境内で、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!とSammytheSharkの画像が表示されます。
完了したら、ファイルを保存して閉じます。
このビューにアクセスするには、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)から、your-server-ipを自分のサーバーのIPアドレスに置き換えて、次のコマンドでDjango開発サーバーを起動します。
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サポートを追加します。
まず、Pythonのパッケージマネージャーであるpipを使用して、DjangoプロジェクトにMozillaのCSPミドルウェアをインストールします。 次のコマンドを使用して、PythonPackageIndexであるPyPiから必要なパッケージをインストールします。 コマンドを実行するには、CONTROL-Cを使用してDjango開発サーバーを停止するか、ターミナルで新しいタブを開きます。
pip install django-csp
次に、ミドルウェアをDjangoプロジェクトの設定に追加します。 settings.pyを開きます:
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を開きます。
nano testsite/testsite/settings.py
ファイル内の任意の場所で次の変数を定義します。
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変数に以下を追加します。
testsite / testsite / settings.py
CSP_IMG_SRC = ("'self'", 'https://html.sammy-codes.com')
現在、このサイトでは、ドメインからの画像のみを許可するのではなく、html.sammy-codes.comからの画像も許可しています。
インデックスビューはGoogleFontsを使用しています。 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-inline、unsafe-eval、unsafe-hashesなどの他のキーワードで使用できます。 CSP。 CSPでこれらのルールを使用しないことを強くお勧めします。 これらは実装を容易にしますが、CSPを回避して役に立たないようにするために使用できます。
詳細については、「安全でないインラインスクリプト」に関するMozilla製品のドキュメントを参照してください。
これで、Google Fontsがサイトにスタイルとフォントをロードできるようになり、html.sammy-codes.comが画像をロードできるようになります。 ただし、サーバー上のページにアクセスすると、現在画像のみが読み込まれていることに気付く場合があります。 これは、フォントの適用に使用されるHTMLのインラインスタイルが許可されていないためです。 次のステップで修正します。
ステップ5—インラインスクリプトとスタイルの操作
この時点で、外部リソースを許可するようにCSPを変更しました。 ただし、ビュー内のスタイルやスクリプトなどのインラインリソースは引き続き許可されていません。 このステップでは、フォントのスタイルを適用できるようにそれらを機能させます。
インラインスクリプトとスタイルを許可するには、ナンスとハッシュの2つの方法があります。 インラインスクリプトとスタイルを頻繁に変更していることがわかった場合は、ナンスを使用してCSPが頻繁に変更されないようにします。 インラインスクリプトとスタイルをめったに更新しない場合は、ハッシュを使用するのが妥当なアプローチです。
nonceを使用してインラインスクリプトを許可する
まず、ナンスアプローチを使用します。 ナンスは、各リクエストに固有のランダムに生成されたトークンです。 2人がサイトにアクセスすると、それぞれが承認したインラインスクリプトとスタイルに埋め込まれた一意のnonceを取得します。 nonceは、サイトの特定の部分を1回のセッションで実行することを承認するワンタイムパスワードと考えてください。
プロジェクトにナンスサポートを追加するには、settings.pyでCSPを更新します。 編集用にファイルを開きます。
nano testsite/testsite/settings.py
settings.pyファイルのCSP_INCLUDE_NONCE_INにscript-srcを追加します。
ファイルの任意の場所にCSP_INCLUDE_NONCE_INを定義し、それに'script-src'を追加します。
testsite / testsite / 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>に次のスニペットを追加します。
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>
完了したら、ファイルを保存して閉じます。
ページを更新すると、スクリプトが実行されます。
Inspect Element を見ると、属性に値がないことがわかります。
セキュリティ上の理由から、この値は表示されません。 ブラウザはすでに値を処理しています。 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に追加して、次のように適用します。
nano testsite/testsite/settings.py
testsite / testsite / settings.py
CSP_STYLE_SRC = ("'self' 'sha256-r5bInLZB0y6ZxHFpmz7cjyYrndjwCeDLDu/1KeMikHA='", 'https://fonts.googleapis.com')
sha256-...ハッシュをCSP_STYLE_SRCリストに追加すると、ブラウザーはエラーなしでスタイルシートをロードできるようになります。
ファイルを保存して閉じます。
ここで、ブラウザにサイトをリロードすると、フォントとスタイルが正常にロードされます。
インラインスタイルとスクリプトが正しく機能するようになりました。 このステップでは、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をダッシュボードの値に置き換えてください。
testsite / testsite / 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を初期化します。 技術的には、CSP違反のためにサーバーでSentryを初期化する必要はありませんが、まれに、ナンスまたはハッシュのレンダリングで問題が発生した場合、これらのエラーはSentryに記録されます。
ファイルを保存して閉じます。
次に、プロジェクトのダッシュボードに戻り、歯車のアイコンをクリックして設定に移動します。
セキュリティヘッダータブに移動します。
report-uriをコピーします。
次のようにCSPに追加します。
testsite / testsite / settings.py
# Content Security Policy CSP_REPORT_URI = "your-report-uri"
必ずyour-report-uriをダッシュボードからコピーした値に置き換えてください。
ファイルを保存して閉じます。 これで、CSPの適用によって違反が発生した場合、SentryはそれをこのURIに記録します。 これを試すには、CSPからドメインまたはハッシュを削除するか、前に追加したスクリプトからnonceを削除します。 ブラウザにページをロードすると、SentryのIssueページにエラーが表示されます。
ログの数に圧倒されている場合は、settings.pyでCSP_REPORT_PERCENTAGEを定義して、ログの一部のみをSentryに送信することもできます。
testsite / testsite / settings.py
# Content Security Policy # Send 10% of the logs to Sentry CSP_REPORT_PERCENTAGE = 0.1
これで、CSP違反が発生するたびに通知が届き、Sentryでエラーを表示できます。
結論
この記事では、コンテンツセキュリティポリシーを使用してDjangoアプリケーションを保護しました。 ポリシーを更新して外部リソースを許可し、ナンスとハッシュを使用してインラインスクリプトとスタイルを許可しました。 また、Sentryに違反を送信するように構成しました。 次のステップとして、 Django CSPドキュメントをチェックして、CSPを適用する方法の詳細を確認してください。