楽しい Django チュートリアル: CSRF、マネージド サービス、ユニコーンについてのストーリー
Google Cloud Japan Team
※この投稿は米国時間 2022 年 4 月 14 日に、Google Cloud blog に投稿されたものの抄訳です。
Django 4.0 のリリースに伴い、CSRF からの保護に関する Django の対処方法に小さな変更があり、送信元ヘッダーがある場合は、確認されるようになりました。具体的に言うと、確認されるのは、URL のスキームです。
この変更は無害で、多くのユーザーにとって影響がないように思えます。ところが、この変更により、Google のチュートリアルを使用した Django 4.0 の Cloud Run へのデプロイはできなくなります。
ただし、App Engine へのデプロイには影響がありません。
これからご紹介するのは、マネージド サービス、ウェブ サーバー ゲートウェイ インターフェース、マジック文字列の深みにはまってしまった 1 人のエンジニア(そう私です)のストーリーです。
ホスティングを管理するマネージド ホスティングについて
マネージド ホスティングを利用する場合、デプロイメントの一部をそのシステムに委ねることになります。スタックの一部について心配する必要はありませんし、プラットホームの SLA の利点を活用できます。しかし、スタックの一部にはアクセスできない設計になっています。
つまり、Cloud Run と App Engine でのサーバーレス ホスティングの場合、ウェブサーバーから先の制御を Google に任せることになります。ユーザーは、コンテナや ZIP ファイルそれぞれでコード一式と、実行するためのコマンドを提供します。その後、Google Cloud は、データが保存されているサーバー、そのサーバーへの電力とネットワーキング、サーバーのメンテナンスなど、アプリケーションに近い重要な部分に至るすべてを扱います。それには、HTTPS アドレスの背後にあるセキュリティなど、デプロイされたサイトへのアクセスに使用するドメインや、アプリケーションにトラフィックを振り向けるプロキシも含まれます。
Cloud Run と App Engine ではアプリケーションの HTTPS URL を指定します。つまり、ユーザーとサーバー間のデータを双方向で暗号化し、TLS 終端を処理するということです。さらに、コンテナ ランタイムの契約に従い、Cloud Run では、受信 HTTPS から HTTP まで、リクエストはコンテナにプロキシされます。これは、後ほど重要になります。
スモーキーな香りのインターフェース
マネージド ホスティングが使用するウェブサーバーを制御することはできないとしても、正しく応答するアプリケーションは必要です。Python デベロッパーの場合、WSGI サーバーを使用することで、すべての処理が行えます。PEP-333 で定義され、後に PEP-3333 に改定された Python ウェブ サーバー ゲートウェイ インターフェース(WSGI)(ウィスキー、またはウィズギーと読みます)は、多くのフレームワークでサポートされています。つまり、選択したウェブ フレームワーク(ここでは Django)で、すべての WSGI サーバーを使用できます。
WSGI は、RFC3875 コモン ゲートウェイ インターフェース(CGI)標準のいくつかの規則を採用していて、これは WSGI 標準に記載されています。これは、後ほど重要になります。
リクエストが届き、レスポンスを返す
HTTP ウェブ アプリケーションでは、さまざまなメソッドに対して応答しています。そして、ウェブサイトのデータに影響を与えない、事実上読み取り専用の「安全」なメソッドがあります。実際の問題は、データを操作できるリクエストの受け入れを開始したときに発生します。これらのメソッドには副作用があり、さらにユーザーデータも含まれています。ユーザーデータは、ウェブ開発において最も危険なものの一つで、信用できません。絶対にです。
多くのウェブ フレームワークでは、SQL インジェクションの緩和や、クロスサイト リクエスト フォージェリ(CSRF)からの保護をはじめとする、ユーザーデータについての一般的な問題に対する保護機能を提供し、デベロッパーをサポートしています。
HTTPS では、リクエスト内容を保護していますが、CSRF 攻撃ではヘッダー情報を標的にし、ユーザーの承認なしに認証済みユーザーの認証情報を使用できるようにします。これは、ユーザーがウェブサイトとやりとりをする必要のあるクリックジャッキングとは異なります。CSRF はいかなるインタラクションも必要とせずに、ウェブ アプリケーションが認証済みユーザーに対して持っている信頼を悪用するのです。
エラー音が聞こえたけど?
Django 1.0 のリリース前から、Django では CSRF 保護機能が提供されていましたが、これまで要求されていた値は、ホスト名だけでした。Django 4.0 では、追加でスキームを提供しなければならないという変更が行われました。たとえば、以前は「mysite.org」だった値が、今では「https://mysite.org」になっています。
CSRF に適した信頼できる送信元の構成は、ALLOWED_HOSTS に似たオプション設定です。ALLOWED_HOSTS は、Django アプリケーションを実行すべきホストを定義できる設定です(ただし、すべてのホストを許可することも可能)。受信リクエストに対して、Django は、HTTP_HOST ヘッダー(CGI 標準から)または、SERVER_NAME(WSGI 標準から)からホストを取得します。このホストが、ALLOWED_HOSTS にない場合はエラーになります。
CSRF 保護機能はより複雑になります。メソッドが「安全でない」場合、Django では、リクエストの送信元が「適切な」送信元と一致するかを検証します。Django は、WSGI サーバーによって提供されるリクエスト スキームを取得し、さまざまな HTTP ヘッダーの一つからホスト名を連結します。
関連するスキームを定義するのは誰か
CSRF を処理するには、スキームについて知ることが重要です。しかし、信頼できる実証済みのメソッドで、スキームを決定できるようにするのは難しい問題です。
CGI では、これについて具体的に定義していませんが、スキーム HTTPS がポート 443 とは異なることを警告し、スクリプトが他のメタデータを使用してスキームを決定することを提案しています。WSGI では、url_scheme と呼ばれるオプションの環境変数を定義していますが、決定方法については定義していません。
この記事の執筆時点で、一般的な Python WSGI サーバーでは、ウェブサーバーからの情報を元に、以下のメソッドを使用して URL スキームを決定しています。
uwsgi では、X-Forwarded-Proto ヘッダーを直接渡します。このヘッダーは、Cloud Run を介して https として返されます。
waitress では TLS を扱っていないので、常に http を返します(waitress-serve を呼び出すときに、--url-scheme https を設定しない限り)。
gunicorn では、定義済みの証明書があるかどうか確認します。また、デフォルトで 127.0.0.1 を含む転送 IP の設定もできます。
Django は wsgi.url_scheme の値を確認します。gunicorn を使用すると(多くの Python サンプルと同じように)、App Engine のウェブサーバーは 127.0.0.1 として実行されるため、App Engine では https を返しますが、Cloud Run では別のプライベート IP を使用するため http を返します。
そのため Cloud Run ではエラーになってしまいます。?
最も正しい解決策
Django アプリケーションに対する正しい解決策は、settings.py ファイルで CSRF_TRUSTED_ORIGINS と ALLOWED_HOSTS 変数を構成することです。私はこれが最も安全な解決策と考えていますが、そのためには、最初にサイトをデプロイする際に追加の手順が必要になります。
Google Cloud での Django に関するチュートリアルは、サービス URL の環境変数を受け取り、その値を各設定に合わせた形式に変換するように更新されました。サービス URL を取得するには、以下のコマンドを実行します。
Cloud Run: gcloud run services describe SERVICE --format "value(status.url)"
App Engine: gcloud app describe --format "value(defaultHostname)"
心配はいりません
アプリケーションが複雑になるにつれて、特にデータの保存や操作を許可する場合、考慮しなければならない問題はより複雑なものになります。アプリケーションの基本となるロジックに十分な情報を提供することで、過去の作業、標準、ベスト プラクティスのすべてを活用できるようになり、それほど心配する必要がなくなります。
- Developer Relations シニア エンジニア Katie McLaughlin